github-search-cli 9.3.0 → 9.4.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/src/cli.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  // Prevent tui.ts auto-launch when imported from cli.ts
2
2
  process.env.GHFIND_CLI_RUN = "1";
3
- import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch, createTrendingSearch } from "./search";
3
+ import {
4
+ parseQuery,
5
+ applyFlagFilters,
6
+ rankRepos,
7
+ createGitHubSearch,
8
+ createTrendingSearch,
9
+ } from "./search";
4
10
  import {
5
11
  createPackageSearch,
6
12
  formatPackagesJson,
@@ -12,8 +18,18 @@ import {
12
18
  type PackageSortMode,
13
19
  } from "./package";
14
20
  import { launchPackageBrowser } from "./package-browser";
15
- import { format as formatOutput, exportToFile, pipeExec, type Format, type ExportFormat, type FormatLine } from "./output";
21
+ import { runDoctor } from "./doctor";
22
+ import { reportError } from "./error-report";
23
+ import {
24
+ format as formatOutput,
25
+ exportToFile,
26
+ pipeExec,
27
+ type Format,
28
+ type ExportFormat,
29
+ type FormatLine,
30
+ } from "./output";
16
31
  import { runWatch } from "./watch";
32
+ import { checkAndNotify, allCachedReleases } from "./releases";
17
33
  import { runInitWizard } from "./init";
18
34
  import { buildComparisonTable } from "./compare";
19
35
  import type { Repo } from "./types";
@@ -40,6 +56,7 @@ interface CLIFlags {
40
56
  pipe?: string;
41
57
  format?: string;
42
58
  watch: boolean;
59
+ releases: boolean;
43
60
  interval: number;
44
61
  completion?: string;
45
62
  init: boolean;
@@ -48,7 +65,8 @@ interface CLIFlags {
48
65
  pkg: boolean;
49
66
  theme?: string;
50
67
  registry: string;
51
- compare: string[]; // repo fullNames to compare
68
+ compare: string[]; // repo fullNames to compare
69
+ doctor: boolean;
52
70
  }
53
71
 
54
72
  function parseArgs(args: string[]): CLIFlags {
@@ -64,6 +82,7 @@ function parseArgs(args: string[]): CLIFlags {
64
82
  trending: false,
65
83
  since: "daily",
66
84
  watch: false,
85
+ releases: false,
67
86
  interval: 300,
68
87
  init: false,
69
88
  version: false,
@@ -71,21 +90,49 @@ function parseArgs(args: string[]): CLIFlags {
71
90
  pkg: false,
72
91
  registry: "npm",
73
92
  compare: [],
93
+ doctor: false,
74
94
  };
75
95
 
76
96
  let queryParts: string[] = [];
77
97
  for (let i = 0; i < args.length; i++) {
78
98
  const arg = args[i];
79
99
  switch (arg) {
80
- case "--json": flags.json = true; break;
81
- case "--csv": flags.csv = true; break;
82
- case "--markdown": flags.markdown = true; break;
83
- case "--count": flags.count = true; break;
84
- case "--trending": flags.trending = true; break;
85
- case "--watch": flags.watch = true; break;
86
- case "--version": case "-v": flags.version = true; break;
87
- case "--help": case "-h": flags.help = true; break;
88
- case "init": flags.init = true; break;
100
+ case "--json":
101
+ flags.json = true;
102
+ break;
103
+ case "--csv":
104
+ flags.csv = true;
105
+ break;
106
+ case "--markdown":
107
+ flags.markdown = true;
108
+ break;
109
+ case "--count":
110
+ flags.count = true;
111
+ break;
112
+ case "--trending":
113
+ flags.trending = true;
114
+ break;
115
+ case "--watch":
116
+ flags.watch = true;
117
+ break;
118
+ case "--releases":
119
+ flags.releases = true;
120
+ break;
121
+ case "--version":
122
+ case "-v":
123
+ flags.version = true;
124
+ break;
125
+ case "--doctor":
126
+ case "doctor":
127
+ flags.doctor = true;
128
+ break;
129
+ case "--help":
130
+ case "-h":
131
+ flags.help = true;
132
+ break;
133
+ case "init":
134
+ flags.init = true;
135
+ break;
89
136
  case "--limit": {
90
137
  const parsed = parseInt(args[++i]);
91
138
  if (isNaN(parsed) || parsed < 1 || parsed > 100) {
@@ -96,14 +143,30 @@ function parseArgs(args: string[]): CLIFlags {
96
143
  }
97
144
  break;
98
145
  }
99
- case "--sort": flags.sort = args[++i] as SearchOptions["sort"]; break;
100
- case "--token": flags.token = args[++i]; break;
101
- case "--since": flags.since = args[++i]; break;
102
- case "--pipe": flags.pipe = args[++i]; break;
103
- case "--format": flags.format = args[++i]; break;
104
- case "--registry": flags.registry = args[++i] ?? "npm"; break;
105
- case "--theme": flags.theme = args[++i] ?? "tokyo-night"; break;
106
- case "--completion": flags.completion = args[++i]; break;
146
+ case "--sort":
147
+ flags.sort = args[++i] as SearchOptions["sort"];
148
+ break;
149
+ case "--token":
150
+ flags.token = args[++i];
151
+ break;
152
+ case "--since":
153
+ flags.since = args[++i];
154
+ break;
155
+ case "--pipe":
156
+ flags.pipe = args[++i];
157
+ break;
158
+ case "--format":
159
+ flags.format = args[++i];
160
+ break;
161
+ case "--registry":
162
+ flags.registry = args[++i] ?? "npm";
163
+ break;
164
+ case "--theme":
165
+ flags.theme = args[++i] ?? "tokyo-night";
166
+ break;
167
+ case "--completion":
168
+ flags.completion = args[++i];
169
+ break;
107
170
  case "--compare": {
108
171
  // Collect repo names until next flag
109
172
  const repos: string[] = [];
@@ -130,11 +193,19 @@ async function main() {
130
193
 
131
194
  // Version
132
195
  if (flags.version) {
133
- const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"));
196
+ const pkg = JSON.parse(
197
+ readFileSync(join(PKG_DIR, "package.json"), "utf-8"),
198
+ );
134
199
  console.log(`ghfind v${pkg.version}`);
135
200
  return;
136
201
  }
137
202
 
203
+ // Doctor — environment diagnostics
204
+ if (flags.doctor) {
205
+ await runDoctor();
206
+ return;
207
+ }
208
+
138
209
  // Help
139
210
  if (flags.help) {
140
211
  console.log(`
@@ -149,6 +220,7 @@ Usage:
149
220
  ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
150
221
  ghfind <query> --pipe <target> Pipe to clone/open
151
222
  ghfind --trending --json Trending repos as JSON
223
+ ghfind --releases Check bookmarks for new releases
152
224
  ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
153
225
  ghfind pkg <query> --json Search npm packages, output JSON
154
226
  ghfind pkg <query> Search npm packages, text list
@@ -156,6 +228,7 @@ Usage:
156
228
  ghfind init Run setup wizard
157
229
  ghfind --completion <shell> Print completion script (bash|zsh|fish)
158
230
  ghfind --version Print version
231
+ ghfind --doctor Run environment diagnostics (troubleshooting)
159
232
  ghfind --help Print this help
160
233
 
161
234
  Options:
@@ -170,6 +243,7 @@ Options:
170
243
  --registry <name> Registry for pkg: npm (default) | others TBD
171
244
 
172
245
  --watch Watch mode (re-run periodically)
246
+ --releases New-release feed for bookmarks
173
247
  --interval <s> Watch interval in seconds (default: 300)
174
248
  --completion <shell> Generate completions
175
249
  `);
@@ -180,7 +254,10 @@ Options:
180
254
  if (flags.completion) {
181
255
  const shell = flags.completion;
182
256
  try {
183
- const content = readFileSync(join(PKG_DIR, "completions", `ghfind.${shell}`), "utf-8");
257
+ const content = readFileSync(
258
+ join(PKG_DIR, "completions", `ghfind.${shell}`),
259
+ "utf-8",
260
+ );
184
261
  console.log(content);
185
262
  } catch {
186
263
  console.error(`Completions not available for shell: ${shell}`);
@@ -197,7 +274,12 @@ Options:
197
274
  // Package search (prototype, ticket 002-008)
198
275
  if (flags.pkg) {
199
276
  const interactive =
200
- !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
277
+ !flags.query &&
278
+ !flags.json &&
279
+ !flags.csv &&
280
+ !flags.markdown &&
281
+ !flags.count &&
282
+ !flags.format;
201
283
  if (interactive) {
202
284
  await launchPackageBrowser();
203
285
  } else {
@@ -207,12 +289,27 @@ Options:
207
289
  }
208
290
 
209
291
  // Determine output format
210
- const outputFormat: ExportFormat | undefined =
211
- flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : undefined;
212
- const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch;
292
+ const outputFormat: ExportFormat | undefined = flags.json
293
+ ? "json"
294
+ : flags.csv
295
+ ? "csv"
296
+ : flags.markdown
297
+ ? "markdown"
298
+ : undefined;
299
+ const isNonInteractive =
300
+ outputFormat ||
301
+ flags.count ||
302
+ flags.format ||
303
+ flags.pipe ||
304
+ flags.watch ||
305
+ flags.releases;
213
306
 
214
307
  // Non-interactive mode
215
308
  if (isNonInteractive) {
309
+ if (flags.releases) {
310
+ await runReleases(flags, outputFormat);
311
+ return;
312
+ }
216
313
  await runNonInteractive(flags, outputFormat);
217
314
  return;
218
315
  }
@@ -241,20 +338,32 @@ async function runPackageSearch(flags: CLIFlags): Promise<void> {
241
338
  console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
242
339
  process.exit(1);
243
340
  }
244
- const PKG_SORTS: PackageSortMode[] = ["best-match", "score", "downloads", "name"];
341
+ const PKG_SORTS: PackageSortMode[] = [
342
+ "best-match",
343
+ "score",
344
+ "downloads",
345
+ "name",
346
+ ];
245
347
  const pkgSort = flags.sort as PackageSortMode;
246
348
  if (!PKG_SORTS.includes(pkgSort)) {
247
- console.error(`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`);
349
+ console.error(
350
+ `Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`,
351
+ );
248
352
  process.exit(1);
249
353
  }
250
354
  const search = createPackageSearch();
251
355
  const query = flags.query.trim();
252
356
  if (!query) {
253
- console.error("Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]");
357
+ console.error(
358
+ "Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]",
359
+ );
254
360
  process.exit(1);
255
361
  }
256
362
  try {
257
- const { totalCount, packages } = await search.searchPackage(query, flags.limit);
363
+ const { totalCount, packages } = await search.searchPackage(
364
+ query,
365
+ flags.limit,
366
+ );
258
367
  const sorted = sortPackages(packages, pkgSort);
259
368
  if (flags.count) {
260
369
  console.log(totalCount);
@@ -270,7 +379,9 @@ async function runPackageSearch(flags: CLIFlags): Promise<void> {
270
379
  console.log(formatPackagesNames(sorted));
271
380
  } else {
272
381
  for (const p of sorted) {
273
- console.log(`${p.name}@${p.version} ↓ ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`);
382
+ console.log(
383
+ `${p.name}@${p.version} ↓ ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`,
384
+ );
274
385
  }
275
386
  }
276
387
  } catch (err) {
@@ -279,15 +390,45 @@ async function runPackageSearch(flags: CLIFlags): Promise<void> {
279
390
  }
280
391
  }
281
392
 
393
+ /** `ghfind --releases` — check bookmarks for new releases; print the cached feed, then mark all seen. */
394
+ async function runReleases(flags: CLIFlags, outputFormat?: ExportFormat): Promise<void> {
395
+ const results = await checkAndNotify({ token: flags.token });
396
+ const releases = allCachedReleases();
397
+ if (outputFormat === "json") {
398
+ console.log(JSON.stringify(releases.map((r) => ({
399
+ fullName: r.fullName, tag: r.tagName, name: r.name,
400
+ url: r.url, publishedAt: r.publishedAt, prerelease: r.prerelease,
401
+ })), null, 2));
402
+ return;
403
+ }
404
+ const unseen = results.reduce((n, r) => n + r.newReleases.length, 0);
405
+ const errors = results.filter((r) => r.error).length;
406
+ console.log(`Checked ${results.length} bookmarked repos — ${unseen} new release${unseen === 1 ? "" : "s"}${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}.`);
407
+ for (const r of releases) {
408
+ console.log(`${r.fullName} ${r.prerelease ? "[pre] " : ""}${r.tagName} ${r.publishedAt.slice(0, 10)}\n ${r.url}`);
409
+ }
410
+ }
411
+
282
412
  async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
283
413
  // Dispatch table: each handler receives shared context + flags, returns void.
284
414
  const handlers: Record<string, (ctx: SearchContext) => Promise<void>> = {
285
415
  trending: async (ctx) => {
286
- const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
416
+ const since =
417
+ flags.since === "weekly"
418
+ ? "weekly"
419
+ : flags.since === "monthly"
420
+ ? "monthly"
421
+ : "daily";
287
422
  const trendingSearch = createTrendingSearch();
288
423
  const response = await trendingSearch.search(
289
424
  { keywords: [], qualifiers: [], raw: "trending" },
290
- { limit: 25, sort: "stars", json: false, verbose: false, trendingSince: since },
425
+ {
426
+ limit: 25,
427
+ sort: "stars",
428
+ json: false,
429
+ verbose: false,
430
+ trendingSince: since,
431
+ },
291
432
  );
292
433
  if (outputFormat) {
293
434
  console.log(formatOutput(response.repos, outputFormat));
@@ -295,21 +436,42 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
295
436
  console.log(response.totalCount);
296
437
  } else {
297
438
  for (const r of response.repos) {
298
- console.log(r.fullName + " ★ " + r.stars + " ▲ +" + r.score + " " + since + " ● " + (r.language ?? ""));
439
+ console.log(
440
+ r.fullName +
441
+ " ★ " +
442
+ r.stars +
443
+ " ▲ +" +
444
+ r.score +
445
+ " " +
446
+ since +
447
+ " ● " +
448
+ (r.language ?? ""),
449
+ );
299
450
  }
300
451
  }
301
452
  },
302
453
  watch: async (ctx) => {
303
454
  let tick = 0;
304
455
  await runWatch(
305
- { query: ctx.query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1000, trending: flags.trending },
456
+ {
457
+ query: ctx.query,
458
+ sort: flags.sort,
459
+ limit: flags.limit,
460
+ token: flags.token,
461
+ intervalMs: flags.interval * 1000,
462
+ trending: flags.trending,
463
+ },
306
464
  (repos, delta) => {
307
465
  tick++;
308
466
  if (outputFormat) {
309
- console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
467
+ console.log(
468
+ `[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`,
469
+ );
310
470
  console.log(formatOutput(repos, outputFormat));
311
471
  } else {
312
- console.log(`[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`);
472
+ console.log(
473
+ `[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`,
474
+ );
313
475
  }
314
476
  },
315
477
  (err) => console.error(`[Watch error] ${err.message}`),
@@ -317,21 +479,33 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
317
479
  },
318
480
  pipe: async (ctx) => {
319
481
  const response = await ctx.provider.search(ctx.parsed, {
320
- limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
482
+ limit: flags.limit,
483
+ sort: flags.sort,
484
+ json: false,
485
+ verbose: false,
486
+ token: flags.token,
321
487
  });
322
488
  const repos = rankRepos(response.repos, flags.sort);
323
489
  await pipeExec(repos, flags.pipe!);
324
490
  },
325
491
  format: async (ctx) => {
326
492
  const response = await ctx.provider.search(ctx.parsed, {
327
- limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
493
+ limit: flags.limit,
494
+ sort: flags.sort,
495
+ json: false,
496
+ verbose: false,
497
+ token: flags.token,
328
498
  });
329
499
  const repos = rankRepos(response.repos, flags.sort);
330
500
  console.log(formatOutput(repos, flags.format as Format));
331
501
  },
332
502
  search: async (ctx) => {
333
503
  const response = await ctx.provider.search(ctx.parsed, {
334
- limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
504
+ limit: flags.limit,
505
+ sort: flags.sort,
506
+ json: false,
507
+ verbose: false,
508
+ token: flags.token,
335
509
  });
336
510
  const repos = rankRepos(response.repos, flags.sort);
337
511
  if (flags.count) {
@@ -348,7 +522,13 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
348
522
  for (const fullName of flags.compare) {
349
523
  const res = await search.search(
350
524
  { keywords: [fullName], qualifiers: [], raw: fullName },
351
- { limit: 1, sort: "stars", json: false, verbose: false, token: flags.token },
525
+ {
526
+ limit: 1,
527
+ sort: "stars",
528
+ json: false,
529
+ verbose: false,
530
+ token: flags.token,
531
+ },
352
532
  );
353
533
  repos.push(...res.repos);
354
534
  }
@@ -357,21 +537,30 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
357
537
  };
358
538
 
359
539
  // Determine which handler to run
360
- const key = flags.compare.length > 0 ? "compare"
361
- : flags.trending && !flags.watch ? "trending"
362
- : flags.watch ? "watch"
363
- : flags.pipe ? "pipe"
364
- : flags.format ? "format"
365
- : "search";
540
+ const key =
541
+ flags.compare.length > 0
542
+ ? "compare"
543
+ : flags.trending && !flags.watch
544
+ ? "trending"
545
+ : flags.watch
546
+ ? "watch"
547
+ : flags.pipe
548
+ ? "pipe"
549
+ : flags.format
550
+ ? "format"
551
+ : "search";
366
552
 
367
553
  const ctx = buildSearchContext(flags);
368
554
  await handlers[key](ctx);
369
555
  }
370
556
 
371
-
372
-
373
557
  main().catch((err) => {
374
- console.error('DEBUG: uncaught error:', err.message || err);
375
- console.error(err instanceof Error ? err.message : String(err));
558
+ if (err instanceof Error && err.name === "SearchCliError") {
559
+ // ponytail: known user-facing errors already carry a friendly message; skip the report prompt
560
+ console.error(err.userMessage ?? err.message);
561
+ } else {
562
+ console.error(err instanceof Error ? err.message : String(err));
563
+ reportError(err);
564
+ }
376
565
  process.exit(1);
377
566
  });