github-search-cli 9.4.1 → 9.4.3
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 +65 -1
- package/README.md +268 -237
- package/bin/ghfind.js +98 -89
- package/completions/ghfind.bash +1 -1
- package/completions/ghfind.fish +3 -1
- package/completions/ghfind.zsh +6 -0
- package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
- package/dist/cli.js +1179 -403
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +16 -5
- package/release-notes/9.3.0.md +32 -29
- package/scripts/build.ts +34 -19
- package/scripts/generate-svg-demo.js +218 -168
- package/scripts/postinstall.js +176 -176
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +650 -566
- package/src/compare.ts +69 -47
- package/src/config.ts +70 -65
- package/src/deepdive.ts +149 -118
- package/src/doctor.ts +287 -287
- package/src/error-report.ts +8 -12
- package/src/errors.ts +62 -59
- package/src/explore.ts +34 -29
- package/src/export.ts +73 -63
- package/src/help.ts +157 -157
- package/src/history.ts +69 -55
- package/src/init.ts +60 -55
- package/src/landing.ts +106 -104
- package/src/login.ts +186 -0
- package/src/markdown-render.ts +1 -6
- package/src/notifications.ts +75 -59
- package/src/open-url.ts +16 -16
- package/src/org.ts +299 -0
- package/src/output.ts +153 -137
- package/src/package-browser.ts +317 -305
- package/src/package.ts +149 -137
- package/src/pipe.ts +60 -54
- package/src/releases.ts +182 -144
- package/src/saved-searches.ts +72 -50
- package/src/search-state.ts +283 -229
- package/src/search.ts +678 -653
- package/src/session.ts +26 -24
- package/src/share.ts +61 -54
- package/src/status.ts +68 -61
- package/src/storage.ts +136 -106
- package/src/themes.ts +259 -116
- package/src/tips.ts +42 -42
- package/src/toast.ts +70 -69
- package/src/trending-parser.ts +104 -0
- package/src/trending.ts +481 -462
- package/src/tui.ts +3015 -2867
- package/src/types.ts +160 -158
- package/src/update-check.ts +231 -215
- package/src/version.ts +33 -0
- package/src/watch.ts +95 -74
package/src/cli.ts
CHANGED
|
@@ -1,566 +1,650 @@
|
|
|
1
|
-
// Prevent tui.ts auto-launch when imported from cli.ts
|
|
2
|
-
process.env.GHFIND_CLI_RUN = "1";
|
|
3
|
-
import {
|
|
4
|
-
parseQuery,
|
|
5
|
-
applyFlagFilters,
|
|
6
|
-
rankRepos,
|
|
7
|
-
createGitHubSearch,
|
|
8
|
-
createTrendingSearch,
|
|
9
|
-
} from "./search";
|
|
10
|
-
import {
|
|
11
|
-
createPackageSearch,
|
|
12
|
-
formatPackagesJson,
|
|
13
|
-
formatPackagesCsv,
|
|
14
|
-
formatPackagesMarkdown,
|
|
15
|
-
formatPackagesUrls,
|
|
16
|
-
formatPackagesNames,
|
|
17
|
-
sortPackages,
|
|
18
|
-
type PackageSortMode,
|
|
19
|
-
} from "./package";
|
|
20
|
-
import { launchPackageBrowser } from "./package-browser";
|
|
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";
|
|
31
|
-
import { runWatch } from "./watch";
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
import {
|
|
40
|
-
import
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
case "
|
|
123
|
-
flags.
|
|
124
|
-
break;
|
|
125
|
-
case "--
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
case "
|
|
147
|
-
flags.
|
|
148
|
-
break;
|
|
149
|
-
case "
|
|
150
|
-
flags.
|
|
151
|
-
break;
|
|
152
|
-
case "--
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
break;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
--
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
--format <fmt>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
--
|
|
246
|
-
--
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (outputFormat) {
|
|
516
|
-
console.log(formatOutput(repos, outputFormat));
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
? "
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1
|
+
// Prevent tui.ts auto-launch when imported from cli.ts
|
|
2
|
+
process.env.GHFIND_CLI_RUN = "1";
|
|
3
|
+
import {
|
|
4
|
+
parseQuery,
|
|
5
|
+
applyFlagFilters,
|
|
6
|
+
rankRepos,
|
|
7
|
+
createGitHubSearch,
|
|
8
|
+
createTrendingSearch,
|
|
9
|
+
} from "./search";
|
|
10
|
+
import {
|
|
11
|
+
createPackageSearch,
|
|
12
|
+
formatPackagesJson,
|
|
13
|
+
formatPackagesCsv,
|
|
14
|
+
formatPackagesMarkdown,
|
|
15
|
+
formatPackagesUrls,
|
|
16
|
+
formatPackagesNames,
|
|
17
|
+
sortPackages,
|
|
18
|
+
type PackageSortMode,
|
|
19
|
+
} from "./package";
|
|
20
|
+
import { launchPackageBrowser } from "./package-browser";
|
|
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";
|
|
31
|
+
import { runWatch } from "./watch";
|
|
32
|
+
import {
|
|
33
|
+
fetchOrgProfile,
|
|
34
|
+
formatOrgJson,
|
|
35
|
+
formatOrgCsv,
|
|
36
|
+
formatOrgMarkdown,
|
|
37
|
+
formatOrgText,
|
|
38
|
+
} from "./org";
|
|
39
|
+
import { checkAndNotify, allCachedReleases } from "./releases";
|
|
40
|
+
import { runInitWizard } from "./init";
|
|
41
|
+
import { runLoginWizard } from "./login";
|
|
42
|
+
import { buildComparisonTable } from "./compare";
|
|
43
|
+
import type { Repo } from "./types";
|
|
44
|
+
import { readFileSync } from "fs";
|
|
45
|
+
import { join, dirname } from "path";
|
|
46
|
+
import { fileURLToPath } from "url";
|
|
47
|
+
import { launchBrowser } from "./tui";
|
|
48
|
+
import { getVersion } from "./version";
|
|
49
|
+
import type { SearchOptions } from "./types";
|
|
50
|
+
import { SearchCliError } from "./errors";
|
|
51
|
+
|
|
52
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
const PKG_DIR = join(__dirname, "..");
|
|
54
|
+
|
|
55
|
+
interface CLIFlags {
|
|
56
|
+
query: string;
|
|
57
|
+
json: boolean;
|
|
58
|
+
csv: boolean;
|
|
59
|
+
markdown: boolean;
|
|
60
|
+
count: boolean;
|
|
61
|
+
limit: number;
|
|
62
|
+
sort: SearchOptions["sort"];
|
|
63
|
+
token?: string;
|
|
64
|
+
trending: boolean;
|
|
65
|
+
since: string;
|
|
66
|
+
pipe?: string;
|
|
67
|
+
format?: string;
|
|
68
|
+
watch: boolean;
|
|
69
|
+
releases: boolean;
|
|
70
|
+
interval: number;
|
|
71
|
+
completion?: string;
|
|
72
|
+
init: boolean;
|
|
73
|
+
login: boolean;
|
|
74
|
+
version: boolean;
|
|
75
|
+
help: boolean;
|
|
76
|
+
pkg: boolean;
|
|
77
|
+
theme?: string;
|
|
78
|
+
registry: string;
|
|
79
|
+
compare: string[]; // repo fullNames to compare
|
|
80
|
+
doctor: boolean;
|
|
81
|
+
org?: string; // org profile lookup (ghfind org <name>)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(args: string[]): CLIFlags {
|
|
85
|
+
const flags: CLIFlags = {
|
|
86
|
+
query: "",
|
|
87
|
+
json: false,
|
|
88
|
+
csv: false,
|
|
89
|
+
markdown: false,
|
|
90
|
+
count: false,
|
|
91
|
+
limit: 50,
|
|
92
|
+
sort: "best-match",
|
|
93
|
+
token: process.env.GITHUB_TOKEN,
|
|
94
|
+
trending: false,
|
|
95
|
+
since: "daily",
|
|
96
|
+
watch: false,
|
|
97
|
+
releases: false,
|
|
98
|
+
interval: 300,
|
|
99
|
+
init: false,
|
|
100
|
+
login: false,
|
|
101
|
+
version: false,
|
|
102
|
+
help: false,
|
|
103
|
+
pkg: false,
|
|
104
|
+
registry: "npm",
|
|
105
|
+
compare: [],
|
|
106
|
+
doctor: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let queryParts: string[] = [];
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
const arg = args[i];
|
|
112
|
+
switch (arg) {
|
|
113
|
+
case "--json":
|
|
114
|
+
flags.json = true;
|
|
115
|
+
break;
|
|
116
|
+
case "--csv":
|
|
117
|
+
flags.csv = true;
|
|
118
|
+
break;
|
|
119
|
+
case "--markdown":
|
|
120
|
+
flags.markdown = true;
|
|
121
|
+
break;
|
|
122
|
+
case "--count":
|
|
123
|
+
flags.count = true;
|
|
124
|
+
break;
|
|
125
|
+
case "--trending":
|
|
126
|
+
flags.trending = true;
|
|
127
|
+
break;
|
|
128
|
+
case "--watch":
|
|
129
|
+
flags.watch = true;
|
|
130
|
+
break;
|
|
131
|
+
case "--releases":
|
|
132
|
+
flags.releases = true;
|
|
133
|
+
break;
|
|
134
|
+
case "--version":
|
|
135
|
+
case "-v":
|
|
136
|
+
flags.version = true;
|
|
137
|
+
break;
|
|
138
|
+
case "--doctor":
|
|
139
|
+
case "doctor":
|
|
140
|
+
flags.doctor = true;
|
|
141
|
+
break;
|
|
142
|
+
case "--help":
|
|
143
|
+
case "-h":
|
|
144
|
+
flags.help = true;
|
|
145
|
+
break;
|
|
146
|
+
case "init":
|
|
147
|
+
flags.init = true;
|
|
148
|
+
break;
|
|
149
|
+
case "login":
|
|
150
|
+
flags.login = true;
|
|
151
|
+
break;
|
|
152
|
+
case "--limit": {
|
|
153
|
+
const parsed = parseInt(args[++i]);
|
|
154
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
155
|
+
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
156
|
+
flags.limit = 50;
|
|
157
|
+
} else {
|
|
158
|
+
flags.limit = parsed;
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "--sort":
|
|
163
|
+
flags.sort = args[++i] as SearchOptions["sort"];
|
|
164
|
+
break;
|
|
165
|
+
case "--token":
|
|
166
|
+
flags.token = args[++i];
|
|
167
|
+
break;
|
|
168
|
+
case "--since":
|
|
169
|
+
flags.since = args[++i];
|
|
170
|
+
break;
|
|
171
|
+
case "--pipe":
|
|
172
|
+
flags.pipe = args[++i];
|
|
173
|
+
break;
|
|
174
|
+
case "--format":
|
|
175
|
+
flags.format = args[++i];
|
|
176
|
+
break;
|
|
177
|
+
case "--registry":
|
|
178
|
+
flags.registry = args[++i] ?? "npm";
|
|
179
|
+
break;
|
|
180
|
+
case "--theme":
|
|
181
|
+
flags.theme = args[++i] ?? "tokyo-night";
|
|
182
|
+
break;
|
|
183
|
+
case "--completion":
|
|
184
|
+
flags.completion = args[++i];
|
|
185
|
+
break;
|
|
186
|
+
case "org": {
|
|
187
|
+
// Collect org name until next flag (allows multi-word quoted names)
|
|
188
|
+
const parts: string[] = [];
|
|
189
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
190
|
+
parts.push(args[++i]);
|
|
191
|
+
}
|
|
192
|
+
flags.org = parts.join("-");
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "--compare": {
|
|
196
|
+
// Collect repo names until next flag
|
|
197
|
+
const repos: string[] = [];
|
|
198
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
199
|
+
repos.push(args[++i]);
|
|
200
|
+
}
|
|
201
|
+
flags.compare = repos;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
default:
|
|
205
|
+
if (!arg.startsWith("-")) queryParts.push(arg);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (queryParts[0] === "pkg") {
|
|
209
|
+
flags.pkg = true;
|
|
210
|
+
queryParts.shift();
|
|
211
|
+
}
|
|
212
|
+
flags.query = queryParts.join(" ");
|
|
213
|
+
return flags;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function main() {
|
|
217
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
218
|
+
|
|
219
|
+
// Version
|
|
220
|
+
if (flags.version) {
|
|
221
|
+
console.log(`ghfind v${getVersion()}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Doctor — environment diagnostics
|
|
226
|
+
if (flags.doctor) {
|
|
227
|
+
await runDoctor();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Help
|
|
232
|
+
if (flags.help) {
|
|
233
|
+
console.log(`
|
|
234
|
+
ghfind — Interactive GitHub repository browser
|
|
235
|
+
|
|
236
|
+
Usage:
|
|
237
|
+
ghfind Launch interactive TUI
|
|
238
|
+
ghfind <query> --json Search, output JSON
|
|
239
|
+
ghfind <query> --csv Search, output CSV
|
|
240
|
+
ghfind <query> --markdown Search, output Markdown table
|
|
241
|
+
ghfind <query> --count Just the result count
|
|
242
|
+
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
243
|
+
ghfind <query> --pipe <target> Pipe to clone/open
|
|
244
|
+
ghfind --trending --json Trending repos as JSON
|
|
245
|
+
ghfind --releases Check bookmarks for new releases
|
|
246
|
+
ghfind --compare <repo1> <repo2> Compare two+ repos side-by-side
|
|
247
|
+
ghfind org <name> --json Org profile: repos, stars, top languages
|
|
248
|
+
ghfind pkg <query> --json Search npm packages, output JSON
|
|
249
|
+
ghfind pkg <query> Search npm packages, text list
|
|
250
|
+
ghfind --watch <query> Watch mode (poll every Ns)
|
|
251
|
+
ghfind init Run setup wizard
|
|
252
|
+
ghfind login Import the gh CLI token or paste one
|
|
253
|
+
ghfind --completion <shell> Print completion script (bash|zsh|fish)
|
|
254
|
+
ghfind --version Print version
|
|
255
|
+
ghfind --doctor Run environment diagnostics (troubleshooting)
|
|
256
|
+
ghfind --help Print this help
|
|
257
|
+
|
|
258
|
+
Options:
|
|
259
|
+
--json, --csv, --markdown, --count Output format
|
|
260
|
+
--limit <n> Max results (default: 50)
|
|
261
|
+
--sort <strategy> Sort: best-match|stars|updated|forks
|
|
262
|
+
--theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
|
|
263
|
+
--trending Trending mode
|
|
264
|
+
--since <period> Trending period: daily|weekly|monthly
|
|
265
|
+
--pipe <target> Pipe target: clone|open
|
|
266
|
+
--format <fmt> Line format: urls|names|ssh-urls|...
|
|
267
|
+
--registry <name> Registry for pkg: npm (default) | others TBD
|
|
268
|
+
|
|
269
|
+
--watch Watch mode (re-run periodically)
|
|
270
|
+
--releases New-release feed for bookmarks
|
|
271
|
+
--interval <s> Watch interval in seconds (default: 300)
|
|
272
|
+
--completion <shell> Generate completions
|
|
273
|
+
`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Shell completions
|
|
278
|
+
if (flags.completion) {
|
|
279
|
+
const shell = flags.completion;
|
|
280
|
+
try {
|
|
281
|
+
const content = readFileSync(
|
|
282
|
+
join(PKG_DIR, "completions", `ghfind.${shell}`),
|
|
283
|
+
"utf-8",
|
|
284
|
+
);
|
|
285
|
+
console.log(content);
|
|
286
|
+
} catch {
|
|
287
|
+
console.error(`Completions not available for shell: ${shell}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Init wizard
|
|
294
|
+
if (flags.init) {
|
|
295
|
+
await runInitWizard();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Login — store a GitHub token for higher rate limits
|
|
300
|
+
if (flags.login) {
|
|
301
|
+
await runLoginWizard();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Package search (prototype, ticket 002-008)
|
|
305
|
+
if (flags.pkg) {
|
|
306
|
+
const interactive =
|
|
307
|
+
!flags.query &&
|
|
308
|
+
!flags.json &&
|
|
309
|
+
!flags.csv &&
|
|
310
|
+
!flags.markdown &&
|
|
311
|
+
!flags.count &&
|
|
312
|
+
!flags.format;
|
|
313
|
+
if (interactive) {
|
|
314
|
+
await launchPackageBrowser();
|
|
315
|
+
} else {
|
|
316
|
+
await runPackageSearch(flags);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Determine output format
|
|
322
|
+
const outputFormat: ExportFormat | undefined = flags.json
|
|
323
|
+
? "json"
|
|
324
|
+
: flags.csv
|
|
325
|
+
? "csv"
|
|
326
|
+
: flags.markdown
|
|
327
|
+
? "markdown"
|
|
328
|
+
: undefined;
|
|
329
|
+
|
|
330
|
+
// Org profile mode — independent of repo search handlers
|
|
331
|
+
// (checks presence, not truthiness, so `ghfind org` with no name errors helpfully)
|
|
332
|
+
if (flags.org !== undefined) {
|
|
333
|
+
await runOrgProfile(flags, outputFormat);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const isNonInteractive =
|
|
338
|
+
outputFormat ||
|
|
339
|
+
flags.count ||
|
|
340
|
+
flags.format ||
|
|
341
|
+
flags.pipe ||
|
|
342
|
+
flags.watch ||
|
|
343
|
+
flags.releases;
|
|
344
|
+
|
|
345
|
+
// Non-interactive mode
|
|
346
|
+
if (isNonInteractive) {
|
|
347
|
+
if (flags.releases) {
|
|
348
|
+
await runReleases(flags, outputFormat);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
await runNonInteractive(flags, outputFormat);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await launchBrowser(flags.theme);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface SearchContext {
|
|
359
|
+
query: string;
|
|
360
|
+
parsed: ReturnType<typeof parseQuery> & { raw: string };
|
|
361
|
+
provider: ReturnType<typeof createGitHubSearch>;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function buildSearchContext(flags: CLIFlags): SearchContext {
|
|
365
|
+
const parsed = applyFlagFilters(parseQuery(flags.query), {});
|
|
366
|
+
return {
|
|
367
|
+
query: flags.query,
|
|
368
|
+
parsed,
|
|
369
|
+
provider: createGitHubSearch(undefined, flags.token ? [flags.token] : []),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** `ghfind org <name>` — aggregate org profile: metadata, stars, top languages, top repos. */
|
|
374
|
+
async function runOrgProfile(
|
|
375
|
+
flags: CLIFlags,
|
|
376
|
+
outputFormat?: ExportFormat,
|
|
377
|
+
): Promise<void> {
|
|
378
|
+
try {
|
|
379
|
+
const profile = await fetchOrgProfile(flags.org!, {
|
|
380
|
+
token: flags.token,
|
|
381
|
+
limit: flags.limit,
|
|
382
|
+
});
|
|
383
|
+
if (outputFormat === "json") {
|
|
384
|
+
console.log(formatOrgJson(profile));
|
|
385
|
+
} else if (outputFormat === "csv") {
|
|
386
|
+
console.log(formatOrgCsv(profile));
|
|
387
|
+
} else if (outputFormat === "markdown") {
|
|
388
|
+
console.log(formatOrgMarkdown(profile));
|
|
389
|
+
} else if (flags.count) {
|
|
390
|
+
console.log(profile.publicRepoCount);
|
|
391
|
+
} else {
|
|
392
|
+
console.log(formatOrgText(profile));
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** `ghfind pkg "<query>"` — npm package search. Pass-through query to the registry. */
|
|
401
|
+
async function runPackageSearch(flags: CLIFlags): Promise<void> {
|
|
402
|
+
if (flags.registry !== "npm") {
|
|
403
|
+
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
const PKG_SORTS: PackageSortMode[] = [
|
|
407
|
+
"best-match",
|
|
408
|
+
"score",
|
|
409
|
+
"downloads",
|
|
410
|
+
"name",
|
|
411
|
+
];
|
|
412
|
+
const pkgSort = flags.sort as PackageSortMode;
|
|
413
|
+
if (!PKG_SORTS.includes(pkgSort)) {
|
|
414
|
+
console.error(
|
|
415
|
+
`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`,
|
|
416
|
+
);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
const search = createPackageSearch();
|
|
420
|
+
const query = flags.query.trim();
|
|
421
|
+
if (!query) {
|
|
422
|
+
console.error(
|
|
423
|
+
"Usage: ghfind pkg <query> [--json|--csv|--markdown|--count|--format urls|names] [--limit <n>]",
|
|
424
|
+
);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const { totalCount, packages } = await search.searchPackage(
|
|
429
|
+
query,
|
|
430
|
+
flags.limit,
|
|
431
|
+
);
|
|
432
|
+
const sorted = sortPackages(packages, pkgSort);
|
|
433
|
+
if (flags.count) {
|
|
434
|
+
console.log(totalCount);
|
|
435
|
+
} else if (flags.json) {
|
|
436
|
+
console.log(formatPackagesJson(sorted));
|
|
437
|
+
} else if (flags.csv) {
|
|
438
|
+
console.log(formatPackagesCsv(sorted));
|
|
439
|
+
} else if (flags.markdown) {
|
|
440
|
+
console.log(formatPackagesMarkdown(sorted));
|
|
441
|
+
} else if (flags.format === "urls") {
|
|
442
|
+
console.log(formatPackagesUrls(sorted));
|
|
443
|
+
} else if (flags.format === "names") {
|
|
444
|
+
console.log(formatPackagesNames(sorted));
|
|
445
|
+
} else {
|
|
446
|
+
for (const p of sorted) {
|
|
447
|
+
console.log(
|
|
448
|
+
`${p.name}@${p.version} ↓ ${p.downloads.toLocaleString()} ${p.score.toFixed(2)} ${p.description ?? ""}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** `ghfind --releases` — check bookmarks for new releases; print the cached feed, then mark all seen. */
|
|
459
|
+
async function runReleases(
|
|
460
|
+
flags: CLIFlags,
|
|
461
|
+
outputFormat?: ExportFormat,
|
|
462
|
+
): Promise<void> {
|
|
463
|
+
const results = await checkAndNotify({ token: flags.token });
|
|
464
|
+
const releases = allCachedReleases();
|
|
465
|
+
if (outputFormat === "json") {
|
|
466
|
+
console.log(
|
|
467
|
+
JSON.stringify(
|
|
468
|
+
releases.map((r) => ({
|
|
469
|
+
fullName: r.fullName,
|
|
470
|
+
tag: r.tagName,
|
|
471
|
+
name: r.name,
|
|
472
|
+
url: r.url,
|
|
473
|
+
publishedAt: r.publishedAt,
|
|
474
|
+
prerelease: r.prerelease,
|
|
475
|
+
})),
|
|
476
|
+
null,
|
|
477
|
+
2,
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const unseen = results.reduce((n, r) => n + r.newReleases.length, 0);
|
|
483
|
+
const errors = results.filter((r) => r.error).length;
|
|
484
|
+
console.log(
|
|
485
|
+
`Checked ${results.length} bookmarked repos — ${unseen} new release${unseen === 1 ? "" : "s"}${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}.`,
|
|
486
|
+
);
|
|
487
|
+
for (const r of releases) {
|
|
488
|
+
console.log(
|
|
489
|
+
`${r.fullName} ${r.prerelease ? "[pre] " : ""}${r.tagName} ${r.publishedAt.slice(0, 10)}\n ${r.url}`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
495
|
+
// Dispatch table: each handler receives shared context + flags, returns void.
|
|
496
|
+
const handlers: Record<string, (ctx: SearchContext) => Promise<void>> = {
|
|
497
|
+
trending: async (ctx) => {
|
|
498
|
+
const since =
|
|
499
|
+
flags.since === "weekly"
|
|
500
|
+
? "weekly"
|
|
501
|
+
: flags.since === "monthly"
|
|
502
|
+
? "monthly"
|
|
503
|
+
: "daily";
|
|
504
|
+
const trendingSearch = createTrendingSearch();
|
|
505
|
+
const response = await trendingSearch.search(
|
|
506
|
+
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
507
|
+
{
|
|
508
|
+
limit: 25,
|
|
509
|
+
sort: "stars",
|
|
510
|
+
json: false,
|
|
511
|
+
verbose: false,
|
|
512
|
+
trendingSince: since,
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
if (outputFormat) {
|
|
516
|
+
console.log(formatOutput(response.repos, outputFormat));
|
|
517
|
+
} else if (flags.count) {
|
|
518
|
+
console.log(response.totalCount);
|
|
519
|
+
} else {
|
|
520
|
+
for (const r of response.repos) {
|
|
521
|
+
console.log(
|
|
522
|
+
r.fullName +
|
|
523
|
+
" ★ " +
|
|
524
|
+
r.stars +
|
|
525
|
+
" ▲ +" +
|
|
526
|
+
r.score +
|
|
527
|
+
" " +
|
|
528
|
+
since +
|
|
529
|
+
" ● " +
|
|
530
|
+
(r.language ?? ""),
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
watch: async (ctx) => {
|
|
536
|
+
let tick = 0;
|
|
537
|
+
await runWatch(
|
|
538
|
+
{
|
|
539
|
+
query: ctx.query,
|
|
540
|
+
sort: flags.sort,
|
|
541
|
+
limit: flags.limit,
|
|
542
|
+
token: flags.token,
|
|
543
|
+
intervalMs: flags.interval * 1000,
|
|
544
|
+
trending: flags.trending,
|
|
545
|
+
},
|
|
546
|
+
(repos, delta) => {
|
|
547
|
+
tick++;
|
|
548
|
+
if (outputFormat) {
|
|
549
|
+
console.log(
|
|
550
|
+
`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`,
|
|
551
|
+
);
|
|
552
|
+
console.log(formatOutput(repos, outputFormat));
|
|
553
|
+
} else {
|
|
554
|
+
console.log(
|
|
555
|
+
`[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
(err) => console.error(`[Watch error] ${err.message}`),
|
|
560
|
+
);
|
|
561
|
+
},
|
|
562
|
+
pipe: async (ctx) => {
|
|
563
|
+
const response = await ctx.provider.search(ctx.parsed, {
|
|
564
|
+
limit: flags.limit,
|
|
565
|
+
sort: flags.sort,
|
|
566
|
+
json: false,
|
|
567
|
+
verbose: false,
|
|
568
|
+
token: flags.token,
|
|
569
|
+
});
|
|
570
|
+
const repos = rankRepos(response.repos, flags.sort);
|
|
571
|
+
await pipeExec(repos, flags.pipe!);
|
|
572
|
+
},
|
|
573
|
+
format: async (ctx) => {
|
|
574
|
+
const response = await ctx.provider.search(ctx.parsed, {
|
|
575
|
+
limit: flags.limit,
|
|
576
|
+
sort: flags.sort,
|
|
577
|
+
json: false,
|
|
578
|
+
verbose: false,
|
|
579
|
+
token: flags.token,
|
|
580
|
+
});
|
|
581
|
+
const repos = rankRepos(response.repos, flags.sort);
|
|
582
|
+
console.log(formatOutput(repos, flags.format as Format));
|
|
583
|
+
},
|
|
584
|
+
search: async (ctx) => {
|
|
585
|
+
const response = await ctx.provider.search(ctx.parsed, {
|
|
586
|
+
limit: flags.limit,
|
|
587
|
+
sort: flags.sort,
|
|
588
|
+
json: false,
|
|
589
|
+
verbose: false,
|
|
590
|
+
token: flags.token,
|
|
591
|
+
});
|
|
592
|
+
const repos = rankRepos(response.repos, flags.sort);
|
|
593
|
+
if (flags.count) {
|
|
594
|
+
console.log(response.totalCount);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (outputFormat) {
|
|
598
|
+
console.log(formatOutput(repos, outputFormat));
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
compare: async (_ctx) => {
|
|
602
|
+
const search = createGitHubSearch(undefined, [flags.token ?? ""]);
|
|
603
|
+
const repos: Repo[] = [];
|
|
604
|
+
for (const fullName of flags.compare) {
|
|
605
|
+
const res = await search.search(
|
|
606
|
+
{ keywords: [fullName], qualifiers: [], raw: fullName },
|
|
607
|
+
{
|
|
608
|
+
limit: 1,
|
|
609
|
+
sort: "stars",
|
|
610
|
+
json: false,
|
|
611
|
+
verbose: false,
|
|
612
|
+
token: flags.token,
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
repos.push(...res.repos);
|
|
616
|
+
}
|
|
617
|
+
console.log(buildComparisonTable(repos));
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// Determine which handler to run
|
|
622
|
+
const key =
|
|
623
|
+
flags.compare.length > 0
|
|
624
|
+
? "compare"
|
|
625
|
+
: flags.trending && !flags.watch
|
|
626
|
+
? "trending"
|
|
627
|
+
: flags.watch
|
|
628
|
+
? "watch"
|
|
629
|
+
: flags.pipe
|
|
630
|
+
? "pipe"
|
|
631
|
+
: flags.format
|
|
632
|
+
? "format"
|
|
633
|
+
: "search";
|
|
634
|
+
|
|
635
|
+
const ctx = buildSearchContext(flags);
|
|
636
|
+
await handlers[key](ctx);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
main().catch((err) => {
|
|
640
|
+
if (err instanceof Error && err.name === "SearchCliError") {
|
|
641
|
+
// ponytail: known user-facing errors already carry a friendly message; skip the report prompt
|
|
642
|
+
console.error(
|
|
643
|
+
err instanceof SearchCliError ? err.userMessage : err.message,
|
|
644
|
+
);
|
|
645
|
+
} else {
|
|
646
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
647
|
+
reportError(err);
|
|
648
|
+
}
|
|
649
|
+
process.exit(1);
|
|
650
|
+
});
|