github-search-cli 9.8.3 → 9.9.0
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 +72 -0
- package/README.md +32 -10
- package/dist/cli.js +543 -67
- package/docs/HOW-TO.md +254 -0
- package/package.json +2 -1
- package/release-notes/9.9.0.md +193 -0
- package/src/cli.ts +13 -11
- package/src/errors.ts +35 -11
- package/src/mcp-server.ts +18 -2
- package/src/search-state.ts +8 -10
- package/src/search.ts +683 -11
- package/src/trending.ts +2 -6
- package/src/tui.ts +55 -17
- package/src/types.ts +11 -1
- package/src/update-check.ts +72 -6
- package/src/watch.ts +16 -11
package/dist/cli.js
CHANGED
|
@@ -24,11 +24,13 @@ var AuthError = class extends SearchCliError {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
var ForbiddenError = class extends SearchCliError {
|
|
27
|
-
constructor() {
|
|
28
|
-
const msg =
|
|
29
|
-
super(
|
|
27
|
+
constructor(detail = "GitHub API returned 403") {
|
|
28
|
+
const msg = `\u26A0 Forbidden (403) \u2014 ${detail}. Common causes: org SSO not authorizing the token, or a secondary rate limit. Press [t] to change the token`;
|
|
29
|
+
super(msg, msg);
|
|
30
|
+
this.detail = detail;
|
|
30
31
|
this.name = "ForbiddenError";
|
|
31
32
|
}
|
|
33
|
+
detail;
|
|
32
34
|
};
|
|
33
35
|
var RateLimitError = class extends SearchCliError {
|
|
34
36
|
constructor(hasToken, resetSeconds) {
|
|
@@ -39,16 +41,36 @@ var RateLimitError = class extends SearchCliError {
|
|
|
39
41
|
}
|
|
40
42
|
hasToken;
|
|
41
43
|
};
|
|
44
|
+
var BadQueryError = class extends SearchCliError {
|
|
45
|
+
/** A concrete, corrected query the user can retry with. */
|
|
46
|
+
constructor(detail, hint) {
|
|
47
|
+
super(
|
|
48
|
+
`Invalid query: ${detail}`,
|
|
49
|
+
hint ? `\u26A0 Invalid query: ${detail}
|
|
50
|
+
Try: ${hint}` : `\u26A0 Invalid query: ${detail}`
|
|
51
|
+
);
|
|
52
|
+
this.detail = detail;
|
|
53
|
+
this.hint = hint;
|
|
54
|
+
this.name = "BadQueryError";
|
|
55
|
+
}
|
|
56
|
+
detail;
|
|
57
|
+
hint;
|
|
58
|
+
};
|
|
42
59
|
var NoResultsError = class extends SearchCliError {
|
|
43
|
-
|
|
60
|
+
/** Concrete loosened queries to retry, most promising first. */
|
|
61
|
+
constructor(query, relaxations = []) {
|
|
62
|
+
const tips = relaxations.length ? relaxations.map((r) => `Try: ${r}`).join("\n ") : "Tips: check spelling, try fewer qualifiers, or [b]rowse trending";
|
|
44
63
|
super(
|
|
45
|
-
`No results for "${query}"`,
|
|
46
|
-
`\u{1F50D} No results for "${query}".
|
|
64
|
+
relaxations.length > 0 ? `No results for "${query}" \u2014 try loosening: ${relaxations.join(" / ")}` : `No results for "${query}"`,
|
|
65
|
+
`\u{1F50D} No results for "${query}".
|
|
66
|
+
${tips}`
|
|
47
67
|
);
|
|
48
68
|
this.query = query;
|
|
69
|
+
this.relaxations = relaxations;
|
|
49
70
|
this.name = "NoResultsError";
|
|
50
71
|
}
|
|
51
72
|
query;
|
|
73
|
+
relaxations;
|
|
52
74
|
};
|
|
53
75
|
var ParseError = class extends SearchCliError {
|
|
54
76
|
constructor(source, detail) {
|
|
@@ -209,9 +231,11 @@ function normalizeEnvelope(env) {
|
|
|
209
231
|
var KNOWN_QUALIFIERS = [
|
|
210
232
|
"language",
|
|
211
233
|
"stars",
|
|
234
|
+
"forks",
|
|
212
235
|
"fork",
|
|
213
236
|
"archived",
|
|
214
237
|
"topic",
|
|
238
|
+
"topics",
|
|
215
239
|
"user",
|
|
216
240
|
"org",
|
|
217
241
|
"repo",
|
|
@@ -221,8 +245,80 @@ var KNOWN_QUALIFIERS = [
|
|
|
221
245
|
"in",
|
|
222
246
|
"size",
|
|
223
247
|
"license",
|
|
224
|
-
"created"
|
|
248
|
+
"created",
|
|
249
|
+
"followers"
|
|
225
250
|
];
|
|
251
|
+
var KNOWN_QUALIFIER_SET = new Set(KNOWN_QUALIFIERS);
|
|
252
|
+
var QUALIFIER_ALIASES = {
|
|
253
|
+
// language
|
|
254
|
+
langauge: "language",
|
|
255
|
+
lang: "language",
|
|
256
|
+
languge: "language",
|
|
257
|
+
// stars / forks
|
|
258
|
+
star: "stars",
|
|
259
|
+
strs: "stars",
|
|
260
|
+
forke: "forks",
|
|
261
|
+
// topic
|
|
262
|
+
topc: "topic",
|
|
263
|
+
tpics: "topic",
|
|
264
|
+
// user / org
|
|
265
|
+
usr: "user",
|
|
266
|
+
ogranization: "org",
|
|
267
|
+
orginzation: "org",
|
|
268
|
+
// date qualifiers
|
|
269
|
+
push: "pushed",
|
|
270
|
+
creat: "created",
|
|
271
|
+
updat: "updated",
|
|
272
|
+
// visibility
|
|
273
|
+
visiblity: "visibility",
|
|
274
|
+
visability: "visibility",
|
|
275
|
+
// license
|
|
276
|
+
licence: "license",
|
|
277
|
+
// size
|
|
278
|
+
siz: "size",
|
|
279
|
+
// archived
|
|
280
|
+
archive: "archived"
|
|
281
|
+
};
|
|
282
|
+
function editDistanceWithin(a, b, max) {
|
|
283
|
+
if (a === b) return 0;
|
|
284
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
285
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
286
|
+
for (let i = 1; i <= a.length; i++) {
|
|
287
|
+
const curr = [i];
|
|
288
|
+
let rowMin = i;
|
|
289
|
+
for (let j = 1; j <= b.length; j++) {
|
|
290
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
291
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
292
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
293
|
+
}
|
|
294
|
+
if (rowMin > max) return max + 1;
|
|
295
|
+
prev = curr;
|
|
296
|
+
}
|
|
297
|
+
return prev[b.length];
|
|
298
|
+
}
|
|
299
|
+
function resolveQualifierKey(key) {
|
|
300
|
+
const k = key.toLowerCase();
|
|
301
|
+
if (KNOWN_QUALIFIER_SET.has(k)) return k;
|
|
302
|
+
const alias = QUALIFIER_ALIASES[k];
|
|
303
|
+
if (alias) return alias;
|
|
304
|
+
if (k.length < 4) return k;
|
|
305
|
+
const maxDistance = k.length <= 5 ? 1 : 2;
|
|
306
|
+
let best;
|
|
307
|
+
let bestDistance = maxDistance + 1;
|
|
308
|
+
let ties = 0;
|
|
309
|
+
for (const known of KNOWN_QUALIFIERS) {
|
|
310
|
+
const d = editDistanceWithin(k, known, maxDistance);
|
|
311
|
+
if (d < bestDistance) {
|
|
312
|
+
bestDistance = d;
|
|
313
|
+
best = known;
|
|
314
|
+
ties = 1;
|
|
315
|
+
if (d === 0) break;
|
|
316
|
+
} else if (d === bestDistance && best !== void 0) {
|
|
317
|
+
ties++;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return best !== void 0 && ties === 1 ? best : k;
|
|
321
|
+
}
|
|
226
322
|
var QUALIFIER_SUGGESTIONS = {
|
|
227
323
|
l: ["language:"],
|
|
228
324
|
s: ["stars:", "size:"],
|
|
@@ -238,13 +334,164 @@ var QUALIFIER_SUGGESTIONS = {
|
|
|
238
334
|
a: ["archived:"]
|
|
239
335
|
};
|
|
240
336
|
function suggestFor(input) {
|
|
241
|
-
const trimmed = input.trim().toLowerCase();
|
|
337
|
+
const trimmed = input.trim().toLowerCase().replace(/:$/, "");
|
|
242
338
|
if (trimmed.length === 0) return [];
|
|
243
339
|
if (trimmed.length === 1 && QUALIFIER_SUGGESTIONS[trimmed]) {
|
|
244
340
|
return QUALIFIER_SUGGESTIONS[trimmed];
|
|
245
341
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
342
|
+
const resolved = resolveQualifierKey(trimmed);
|
|
343
|
+
const aliasHit = resolved !== trimmed && KNOWN_QUALIFIER_SET.has(resolved) ? [`${resolved}:`] : [];
|
|
344
|
+
const matches = KNOWN_QUALIFIERS.filter(
|
|
345
|
+
(k) => k.startsWith(trimmed) && !aliasHit.includes(`${k}:`)
|
|
346
|
+
).map((k) => `${k}:`);
|
|
347
|
+
return [...aliasHit, ...matches];
|
|
348
|
+
}
|
|
349
|
+
var NUMERIC_QUALIFIERS = /* @__PURE__ */ new Set([
|
|
350
|
+
"forks",
|
|
351
|
+
"size",
|
|
352
|
+
"stars",
|
|
353
|
+
"topics",
|
|
354
|
+
"followers"
|
|
355
|
+
]);
|
|
356
|
+
var BOOLEAN_QUALIFIERS = /* @__PURE__ */ new Set(["archived"]);
|
|
357
|
+
var VISIBILITY_VALUES = /* @__PURE__ */ new Set(["public", "private", "internal"]);
|
|
358
|
+
var IN_FIELDS = /* @__PURE__ */ new Set([
|
|
359
|
+
"name",
|
|
360
|
+
"description",
|
|
361
|
+
"topics",
|
|
362
|
+
"readme",
|
|
363
|
+
"comments"
|
|
364
|
+
]);
|
|
365
|
+
var DATE_QUALIFIERS = /* @__PURE__ */ new Set(["created", "pushed", "updated"]);
|
|
366
|
+
var COMPARISON_OPERATORS = [">=", "<=", ">", "<"];
|
|
367
|
+
function redactToken(url, token) {
|
|
368
|
+
return token ? url.replace(token, "<token>") : url;
|
|
369
|
+
}
|
|
370
|
+
function splitComparison(value) {
|
|
371
|
+
for (const op of COMPARISON_OPERATORS) {
|
|
372
|
+
if (value.startsWith(op)) {
|
|
373
|
+
return { op, rest: value.slice(op.length) };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return { op: "", rest: value };
|
|
377
|
+
}
|
|
378
|
+
function parseCount(raw) {
|
|
379
|
+
const trimmed = raw.trim().replace(/_/g, "");
|
|
380
|
+
if (trimmed === "") return null;
|
|
381
|
+
const match = /^(\d+(?:\.\d+)?)([km])?$/i.exec(trimmed);
|
|
382
|
+
if (!match) return null;
|
|
383
|
+
if (!match[2] && match[1].includes(".")) return null;
|
|
384
|
+
const base = Number.parseFloat(match[1]);
|
|
385
|
+
if (!Number.isFinite(base)) return null;
|
|
386
|
+
const suffix = match[2]?.toLowerCase();
|
|
387
|
+
const multiplier = suffix === "k" ? 1e3 : suffix === "m" ? 1e6 : 1;
|
|
388
|
+
return Math.floor(base * multiplier);
|
|
389
|
+
}
|
|
390
|
+
function isValidISODate(raw) {
|
|
391
|
+
const trimmed = raw.trim();
|
|
392
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(?:Z|[+-]\d{2}:?\d{2})?)?$/.exec(
|
|
393
|
+
trimmed
|
|
394
|
+
);
|
|
395
|
+
if (!m) return false;
|
|
396
|
+
const [, y, mo, d, h = "0", mi = "0", s = "0"] = m;
|
|
397
|
+
const year = Number(y);
|
|
398
|
+
const month = Number(mo);
|
|
399
|
+
const day = Number(d);
|
|
400
|
+
if (month < 1 || month > 12) return false;
|
|
401
|
+
if (day < 1 || day > 31) return false;
|
|
402
|
+
if (Number(h) > 23 || Number(mi) > 59 || Number(s) > 59) return false;
|
|
403
|
+
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
404
|
+
return day <= daysInMonth;
|
|
405
|
+
}
|
|
406
|
+
function normalizeQualifierValue(key, value) {
|
|
407
|
+
const normalizedKey = key.toLowerCase();
|
|
408
|
+
if (!NUMERIC_QUALIFIERS.has(normalizedKey)) return value;
|
|
409
|
+
if (value === "") return value;
|
|
410
|
+
const { op, rest } = splitComparison(value);
|
|
411
|
+
const rangeIdx = rest.indexOf("..");
|
|
412
|
+
if (rangeIdx > 0) {
|
|
413
|
+
const lo = parseCount(rest.slice(0, rangeIdx));
|
|
414
|
+
const hi = parseCount(rest.slice(rangeIdx + 2));
|
|
415
|
+
if (lo === null || hi === null) return value;
|
|
416
|
+
return `${op}${lo}..${hi}`;
|
|
417
|
+
}
|
|
418
|
+
if (!/[km]$/i.test(rest.trim())) return value;
|
|
419
|
+
const count = parseCount(rest);
|
|
420
|
+
if (count === null) return value;
|
|
421
|
+
return `${op || ">="}${count}`;
|
|
422
|
+
}
|
|
423
|
+
function validateQualifierValue(key, value) {
|
|
424
|
+
const k = key.toLowerCase();
|
|
425
|
+
if (!KNOWN_QUALIFIER_SET.has(k)) return null;
|
|
426
|
+
if (value.trim() === "") {
|
|
427
|
+
return `"${key}:" needs a value (e.g. ${key}:<something>)`;
|
|
428
|
+
}
|
|
429
|
+
if (NUMERIC_QUALIFIERS.has(k)) {
|
|
430
|
+
const { rest } = splitComparison(value.trim());
|
|
431
|
+
const rangeIdx = rest.indexOf("..");
|
|
432
|
+
if (rangeIdx > 0) {
|
|
433
|
+
const lo = parseCount(rest.slice(0, rangeIdx));
|
|
434
|
+
const hi = parseCount(rest.slice(rangeIdx + 2));
|
|
435
|
+
if (lo === null || hi === null) {
|
|
436
|
+
return `"${key}:${value}" is not a valid range \u2014 use ${k}:100..500 or ${k}:>=100`;
|
|
437
|
+
}
|
|
438
|
+
if (lo > hi) {
|
|
439
|
+
return `"${key}:${value}" has a reversed range (${lo} > ${hi})`;
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
if (parseCount(rest) === null) {
|
|
444
|
+
return `"${key}:${value}" is not a number \u2014 use ${k}:100, ${k}:>=100, or ${k}:1k..10k`;
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
if (k === "fork") {
|
|
449
|
+
const v = value.trim().toLowerCase();
|
|
450
|
+
if (v !== "true" && v !== "false" && v !== "only") {
|
|
451
|
+
return `"fork:${value}" is not valid \u2014 use fork:true, fork:false, or fork:only`;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
if (BOOLEAN_QUALIFIERS.has(k)) {
|
|
456
|
+
const v = value.trim().toLowerCase();
|
|
457
|
+
if (v !== "true" && v !== "false") {
|
|
458
|
+
return `"${key}:${value}" is not valid \u2014 use ${k}:true or ${k}:false`;
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
if (k === "visibility") {
|
|
463
|
+
const v = value.trim().toLowerCase();
|
|
464
|
+
if (!VISIBILITY_VALUES.has(v)) {
|
|
465
|
+
return `"visibility:${value}" is not valid \u2014 use visibility:public, visibility:private, or visibility:internal`;
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
if (k === "in") {
|
|
470
|
+
const parts = value.split(",").map((p) => p.trim().toLowerCase());
|
|
471
|
+
if (parts.some((p) => !IN_FIELDS.has(p))) {
|
|
472
|
+
const bad = parts.find((p) => !IN_FIELDS.has(p));
|
|
473
|
+
return `"in:${value}" is not valid \u2014 ${bad ? `"${bad}" is not a ` : ""}searchable field. Try in:name, in:description, in:topics, or in:readme`;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (DATE_QUALIFIERS.has(k)) {
|
|
478
|
+
const trimmed = value.trim();
|
|
479
|
+
const { rest } = splitComparison(trimmed);
|
|
480
|
+
const rangeIdx = rest.indexOf("..");
|
|
481
|
+
if (rangeIdx > 0) {
|
|
482
|
+
const lo = rest.slice(0, rangeIdx);
|
|
483
|
+
const hi = rest.slice(rangeIdx + 2);
|
|
484
|
+
if (!isValidISODate(lo) || !isValidISODate(hi)) {
|
|
485
|
+
return `"${key}:${value}" is not a valid date range \u2014 use ${k}:2020-01-01..2024-01-01`;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
if (!isValidISODate(rest)) {
|
|
490
|
+
return `"${key}:${value}" is not a valid date \u2014 use ${k}:2024-01-01 (optionally with >, >=, <, <=)`;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
248
495
|
}
|
|
249
496
|
function tokenize(input) {
|
|
250
497
|
const tokens = [];
|
|
@@ -272,22 +519,46 @@ function tokenize(input) {
|
|
|
272
519
|
}
|
|
273
520
|
return tokens;
|
|
274
521
|
}
|
|
522
|
+
var EMPTY_QUERY = {
|
|
523
|
+
keywords: [],
|
|
524
|
+
qualifiers: [],
|
|
525
|
+
raw: "",
|
|
526
|
+
corrections: []
|
|
527
|
+
};
|
|
528
|
+
function trendingQuery() {
|
|
529
|
+
return { ...EMPTY_QUERY, raw: "trending" };
|
|
530
|
+
}
|
|
275
531
|
function parseQuery(raw) {
|
|
276
532
|
const trimmed = raw.trim();
|
|
277
533
|
if (trimmed === "") {
|
|
278
|
-
return { keywords: [], qualifiers: [], raw };
|
|
534
|
+
return { keywords: [], qualifiers: [], raw, corrections: [] };
|
|
279
535
|
}
|
|
280
536
|
const keywords = [];
|
|
281
537
|
const qualifiers = [];
|
|
538
|
+
const corrections = [];
|
|
282
539
|
for (const token of tokenize(trimmed)) {
|
|
283
540
|
const qualifier = matchQualifier(token);
|
|
284
541
|
if (qualifier) {
|
|
285
|
-
|
|
542
|
+
const resolvedKey = resolveQualifierKey(qualifier.key);
|
|
543
|
+
const normalizedValue = normalizeQualifierValue(
|
|
544
|
+
resolvedKey,
|
|
545
|
+
qualifier.value
|
|
546
|
+
);
|
|
547
|
+
const original = `${qualifier.negated ? "-" : ""}${qualifier.key}:${qualifier.value}`;
|
|
548
|
+
const corrected = `${qualifier.negated ? "-" : ""}${resolvedKey}:${normalizedValue}`;
|
|
549
|
+
if (original !== corrected) {
|
|
550
|
+
corrections.push({ from: original, to: corrected });
|
|
551
|
+
}
|
|
552
|
+
qualifiers.push({
|
|
553
|
+
key: resolvedKey,
|
|
554
|
+
value: normalizedValue,
|
|
555
|
+
negated: qualifier.negated
|
|
556
|
+
});
|
|
286
557
|
} else {
|
|
287
558
|
keywords.push(token);
|
|
288
559
|
}
|
|
289
560
|
}
|
|
290
|
-
return { keywords, qualifiers, raw: trimmed };
|
|
561
|
+
return { keywords, qualifiers, raw: trimmed, corrections };
|
|
291
562
|
}
|
|
292
563
|
function matchQualifier(token) {
|
|
293
564
|
const negated = token.startsWith("-");
|
|
@@ -296,8 +567,7 @@ function matchQualifier(token) {
|
|
|
296
567
|
if (idx <= 0) return null;
|
|
297
568
|
const key = body.slice(0, idx).toLowerCase();
|
|
298
569
|
let value = body.slice(idx + 1);
|
|
299
|
-
if (
|
|
300
|
-
if (!/^[a-z]+$/i.test(key)) return null;
|
|
570
|
+
if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/i.test(key)) return null;
|
|
301
571
|
if (value.startsWith("//")) return null;
|
|
302
572
|
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
303
573
|
value = value.slice(1, -1);
|
|
@@ -330,11 +600,143 @@ function applyFlagFilters(query, flags) {
|
|
|
330
600
|
});
|
|
331
601
|
return { ...query, qualifiers };
|
|
332
602
|
}
|
|
603
|
+
function withoutQualifier(query, index) {
|
|
604
|
+
const remaining = query.qualifiers.filter((_, i) => i !== index);
|
|
605
|
+
const parts = [...query.keywords];
|
|
606
|
+
for (const q of remaining) {
|
|
607
|
+
const needsQuote = q.value.includes(" ") && !q.value.startsWith('"');
|
|
608
|
+
const value = needsQuote ? `"${q.value}"` : q.value;
|
|
609
|
+
parts.push(`${q.negated ? "-" : ""}${q.key}:${value}`);
|
|
610
|
+
}
|
|
611
|
+
const text = parts.join(" ").trim();
|
|
612
|
+
return text === "" ? null : text;
|
|
613
|
+
}
|
|
333
614
|
function validateQuery(query) {
|
|
334
615
|
const has = (key) => query.qualifiers.some((q) => q.key === key && !q.negated);
|
|
335
616
|
if (has("visibility") && has("private")) {
|
|
336
|
-
throw new
|
|
617
|
+
throw new BadQueryError(
|
|
618
|
+
"Cannot combine `visibility:` and `private:` filters.",
|
|
619
|
+
`${query.qualifiers.filter((q) => q.key === "private" && !q.negated).map((q) => withoutQualifier(query, query.qualifiers.indexOf(q))).find((v) => v !== null) ?? query.raw}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
for (let i = 0; i < query.qualifiers.length; i++) {
|
|
623
|
+
const q = query.qualifiers[i];
|
|
624
|
+
const problem = validateQualifierValue(q.key, q.value);
|
|
625
|
+
if (problem) {
|
|
626
|
+
throw new BadQueryError(problem, withoutQualifier(query, i) ?? void 0);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
var OVER_CONSTRAINT_WEIGHT = {
|
|
631
|
+
size: 100,
|
|
632
|
+
stars: 95,
|
|
633
|
+
forks: 80,
|
|
634
|
+
created: 75,
|
|
635
|
+
pushed: 70,
|
|
636
|
+
updated: 70,
|
|
637
|
+
visibility: 65,
|
|
638
|
+
repo: 60,
|
|
639
|
+
user: 55,
|
|
640
|
+
org: 50,
|
|
641
|
+
topic: 45,
|
|
642
|
+
in: 30,
|
|
643
|
+
license: 25,
|
|
644
|
+
language: 20,
|
|
645
|
+
fork: 15,
|
|
646
|
+
archived: 10
|
|
647
|
+
};
|
|
648
|
+
function loosenNumericBound(value) {
|
|
649
|
+
const { op, rest } = splitComparison(value.trim());
|
|
650
|
+
const count = parseCount(rest);
|
|
651
|
+
if (count === null || count === 0) return null;
|
|
652
|
+
const loosened = Math.max(1, Math.floor(count / 10));
|
|
653
|
+
return `${op || ">="}${loosened}`;
|
|
654
|
+
}
|
|
655
|
+
function loosenDateBound(value) {
|
|
656
|
+
const { op, rest } = splitComparison(value.trim());
|
|
657
|
+
if (!isValidISODate(rest)) return null;
|
|
658
|
+
const year = Number.parseInt(rest.slice(0, 4), 10);
|
|
659
|
+
if (!Number.isFinite(year)) return null;
|
|
660
|
+
const shifted = op === "<" || op === "<=" ? year + 2 : Math.max(1970, year - 2);
|
|
661
|
+
return { op, date: `${shifted}${rest.slice(4)}` };
|
|
662
|
+
}
|
|
663
|
+
function suggestRelaxations(query, limit = 3) {
|
|
664
|
+
const out = [];
|
|
665
|
+
for (let i = 0; i < query.qualifiers.length; i++) {
|
|
666
|
+
const q = query.qualifiers[i];
|
|
667
|
+
const key = q.key.toLowerCase();
|
|
668
|
+
if (q.negated) continue;
|
|
669
|
+
const weight = OVER_CONSTRAINT_WEIGHT[key] ?? 40;
|
|
670
|
+
const emit = (relaxed, qualifier) => {
|
|
671
|
+
const rest = query.qualifiers.filter((_, j) => j !== i).map((other) => {
|
|
672
|
+
const needsQuote = other.value.includes(" ") && !other.value.startsWith('"');
|
|
673
|
+
const value = needsQuote ? `"${other.value}"` : other.value;
|
|
674
|
+
return `${other.negated ? "-" : ""}${other.key}:${value}`;
|
|
675
|
+
});
|
|
676
|
+
const text = [...query.keywords, ...rest, relaxed].join(" ").trim();
|
|
677
|
+
out.push({ suggestion: text, qualifier, confidence: weight });
|
|
678
|
+
};
|
|
679
|
+
if (NUMERIC_QUALIFIERS.has(key) || key === "followers") {
|
|
680
|
+
const rangeIdx = q.value.indexOf("..");
|
|
681
|
+
if (rangeIdx > 0) {
|
|
682
|
+
const hi = parseCount(q.value.slice(rangeIdx + 2));
|
|
683
|
+
if (hi !== null) {
|
|
684
|
+
const lo = q.value.slice(0, rangeIdx);
|
|
685
|
+
emit(`${key}:${lo}..${Math.floor(hi / 10) || hi * 2}`, key);
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
const looser = loosenNumericBound(q.value);
|
|
689
|
+
if (looser) emit(`${key}:${looser}`, key);
|
|
690
|
+
else {
|
|
691
|
+
const dropped2 = withoutQualifier(query, i);
|
|
692
|
+
if (dropped2) {
|
|
693
|
+
out.push({
|
|
694
|
+
suggestion: dropped2,
|
|
695
|
+
qualifier: key,
|
|
696
|
+
confidence: weight - 30
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (DATE_QUALIFIERS.has(key)) {
|
|
704
|
+
const loosened = loosenDateBound(q.value);
|
|
705
|
+
if (loosened) {
|
|
706
|
+
emit(`${key}:${loosened.op || ">="}${loosened.date}`, key);
|
|
707
|
+
} else {
|
|
708
|
+
const dropped2 = withoutQualifier(query, i);
|
|
709
|
+
if (dropped2) {
|
|
710
|
+
out.push({
|
|
711
|
+
suggestion: dropped2,
|
|
712
|
+
qualifier: key,
|
|
713
|
+
confidence: weight - 30
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const dropped = withoutQualifier(query, i);
|
|
720
|
+
if (dropped) {
|
|
721
|
+
out.push({
|
|
722
|
+
suggestion: dropped,
|
|
723
|
+
qualifier: key,
|
|
724
|
+
confidence: weight - 30
|
|
725
|
+
});
|
|
726
|
+
}
|
|
337
727
|
}
|
|
728
|
+
const seen = /* @__PURE__ */ new Set();
|
|
729
|
+
const unique = out.filter((r) => {
|
|
730
|
+
if (seen.has(r.suggestion)) return false;
|
|
731
|
+
seen.add(r.suggestion);
|
|
732
|
+
return true;
|
|
733
|
+
});
|
|
734
|
+
unique.sort((a, b) => b.confidence - a.confidence);
|
|
735
|
+
return unique.slice(0, limit).map((r) => ({
|
|
736
|
+
...r,
|
|
737
|
+
// Clamp so a "drop the qualifier" hint never reports a negative score.
|
|
738
|
+
confidence: Math.max(0, r.confidence)
|
|
739
|
+
}));
|
|
338
740
|
}
|
|
339
741
|
function buildGitHubQuery(query) {
|
|
340
742
|
const parts = [];
|
|
@@ -431,6 +833,11 @@ var GitHubSearchAdapter = class {
|
|
|
431
833
|
});
|
|
432
834
|
const remainingStr = res.headers.get("x-ratelimit-remaining");
|
|
433
835
|
rateLimitRemaining = remainingStr !== null ? Number(remainingStr) : void 0;
|
|
836
|
+
if (process.env.GHFIND_LOG) {
|
|
837
|
+
this.logger.debug(
|
|
838
|
+
`[github] ${res.status} remaining=${remainingStr ?? "none"} url=${redactToken(pagedUrl, token)}`
|
|
839
|
+
);
|
|
840
|
+
}
|
|
434
841
|
if (res.status === 403) {
|
|
435
842
|
if (remainingStr === "0" || remainingStr === null) {
|
|
436
843
|
if (this.tokens.length > 0 && attempt < this.tokens.length) {
|
|
@@ -442,9 +849,18 @@ var GitHubSearchAdapter = class {
|
|
|
442
849
|
}
|
|
443
850
|
const body = await res.text().catch(() => "");
|
|
444
851
|
this.logger.error(
|
|
445
|
-
`[github] non-rate-limit 403:
|
|
852
|
+
`[github] non-rate-limit 403: remaining=${remainingStr} retry-after=${res.headers.get("retry-after") ?? "none"} x-ratelimit-reset=${res.headers.get("x-ratelimit-reset") ?? "none"} authed=${token ? "yes" : "no"} url=${redactToken(pagedUrl, token)}`
|
|
446
853
|
);
|
|
447
|
-
|
|
854
|
+
this.logger.error(`[github] 403 body: ${body.slice(0, 400)}`);
|
|
855
|
+
let reason = "no reason given";
|
|
856
|
+
try {
|
|
857
|
+
const parsed2 = JSON.parse(body);
|
|
858
|
+
if (typeof parsed2.message === "string" && parsed2.message) {
|
|
859
|
+
reason = parsed2.message;
|
|
860
|
+
}
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
throw new ForbiddenError(reason);
|
|
448
864
|
}
|
|
449
865
|
if (res.status === 401) {
|
|
450
866
|
const body = await res.text().catch(() => "");
|
|
@@ -1737,7 +2153,7 @@ var cached = null;
|
|
|
1737
2153
|
function getVersion() {
|
|
1738
2154
|
if (cached) return cached;
|
|
1739
2155
|
if (true) {
|
|
1740
|
-
cached = "9.
|
|
2156
|
+
cached = "9.9.0";
|
|
1741
2157
|
return cached;
|
|
1742
2158
|
}
|
|
1743
2159
|
try {
|
|
@@ -1996,22 +2412,22 @@ async function runWatch(opts, onChange, onError) {
|
|
|
1996
2412
|
let previous = [];
|
|
1997
2413
|
let failCount = 0;
|
|
1998
2414
|
let intervalId = null;
|
|
2415
|
+
if (!opts.trending) {
|
|
2416
|
+
validateQuery(applyFlagFilters(parseQuery(opts.query), {}));
|
|
2417
|
+
}
|
|
1999
2418
|
const tick = async () => {
|
|
2000
2419
|
try {
|
|
2001
2420
|
let repos;
|
|
2002
2421
|
if (opts.trending) {
|
|
2003
2422
|
const since = opts.query === "weekly" ? "weekly" : opts.query === "monthly" ? "monthly" : "daily";
|
|
2004
2423
|
const trendingSearch = createTrendingSearch();
|
|
2005
|
-
const response = await trendingSearch.search(
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
trendingSince: since
|
|
2013
|
-
}
|
|
2014
|
-
);
|
|
2424
|
+
const response = await trendingSearch.search(trendingQuery(), {
|
|
2425
|
+
limit: opts.limit,
|
|
2426
|
+
sort: opts.sort,
|
|
2427
|
+
json: false,
|
|
2428
|
+
verbose: false,
|
|
2429
|
+
trendingSince: since
|
|
2430
|
+
});
|
|
2015
2431
|
repos = response.repos;
|
|
2016
2432
|
} else {
|
|
2017
2433
|
const parsed = applyFlagFilters(parseQuery(opts.query), {});
|
|
@@ -3422,6 +3838,7 @@ import {
|
|
|
3422
3838
|
SelectRenderable,
|
|
3423
3839
|
ScrollBoxRenderable as ScrollBoxRenderable3
|
|
3424
3840
|
} from "@opentui/core";
|
|
3841
|
+
import { appendFileSync as appendFileSync2 } from "fs";
|
|
3425
3842
|
|
|
3426
3843
|
// src/trending.ts
|
|
3427
3844
|
import {
|
|
@@ -4455,19 +4872,40 @@ function shouldCheckUpdate() {
|
|
|
4455
4872
|
if (Date.now() - state.lastCheck < CHECK_INTERVAL_MS) return false;
|
|
4456
4873
|
return true;
|
|
4457
4874
|
}
|
|
4875
|
+
function semverParts(v) {
|
|
4876
|
+
const core = v.trim().replace(/^v/i, "").split(/[-+]/)[0];
|
|
4877
|
+
return core.split(".").map((s) => {
|
|
4878
|
+
const n = Number(s);
|
|
4879
|
+
return Number.isFinite(n) ? n : 0;
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
function isPrerelease(v) {
|
|
4883
|
+
const core = v.trim().replace(/^v/i, "");
|
|
4884
|
+
const plus = core.indexOf("+");
|
|
4885
|
+
const coreOnly = plus === -1 ? core : core.slice(0, plus);
|
|
4886
|
+
return coreOnly.includes("-");
|
|
4887
|
+
}
|
|
4458
4888
|
function isNewerVersion(current, latest) {
|
|
4459
4889
|
if (typeof Bun !== "undefined" && Bun.semver) {
|
|
4460
4890
|
return Bun.semver.order(current, latest) < 0;
|
|
4461
4891
|
}
|
|
4462
|
-
const curParts = current
|
|
4463
|
-
const newParts = latest
|
|
4892
|
+
const curParts = semverParts(current);
|
|
4893
|
+
const newParts = semverParts(latest);
|
|
4894
|
+
let order = 0;
|
|
4464
4895
|
for (let i = 0; i < Math.max(curParts.length, newParts.length); i++) {
|
|
4465
4896
|
const c = curParts[i] ?? 0;
|
|
4466
4897
|
const n = newParts[i] ?? 0;
|
|
4467
|
-
if (n > c)
|
|
4468
|
-
|
|
4898
|
+
if (n > c) {
|
|
4899
|
+
order = 1;
|
|
4900
|
+
break;
|
|
4901
|
+
}
|
|
4902
|
+
if (n < c) {
|
|
4903
|
+
order = -1;
|
|
4904
|
+
break;
|
|
4905
|
+
}
|
|
4469
4906
|
}
|
|
4470
|
-
return
|
|
4907
|
+
if (order !== 0) return order > 0;
|
|
4908
|
+
return isPrerelease(current) && !isPrerelease(latest);
|
|
4471
4909
|
}
|
|
4472
4910
|
async function checkForUpdate(currentVersion) {
|
|
4473
4911
|
if (process.env.DEBUG)
|
|
@@ -4574,6 +5012,17 @@ function recordPreUpdateState(currentVersion) {
|
|
|
4574
5012
|
lastInstalledVersion: currentVersion
|
|
4575
5013
|
});
|
|
4576
5014
|
}
|
|
5015
|
+
function markPostUpgradeSeen(currentVersion) {
|
|
5016
|
+
try {
|
|
5017
|
+
const state = readUpdateState();
|
|
5018
|
+
if (state.lastInstalledVersion === currentVersion) return;
|
|
5019
|
+
if (isNewerVersion(currentVersion, state.lastInstalledVersion ?? "")) {
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
writeUpdateState({ ...state, lastInstalledVersion: currentVersion });
|
|
5023
|
+
} catch {
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
4577
5026
|
function fetchReleaseNotes(version) {
|
|
4578
5027
|
try {
|
|
4579
5028
|
const notesPath = join6(PKG_DIR2, "release-notes", `${version}.md`);
|
|
@@ -4648,16 +5097,33 @@ var colors = {
|
|
|
4648
5097
|
surfaceDim: "#1a1919",
|
|
4649
5098
|
borderAccent: "#1a1919"
|
|
4650
5099
|
};
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
}
|
|
4660
|
-
|
|
5100
|
+
function createTuiLogger() {
|
|
5101
|
+
const target = process.env.GHFIND_LOG;
|
|
5102
|
+
if (!target) {
|
|
5103
|
+
return { debug: () => {
|
|
5104
|
+
}, info: () => {
|
|
5105
|
+
}, warn: () => {
|
|
5106
|
+
}, error: () => {
|
|
5107
|
+
} };
|
|
5108
|
+
}
|
|
5109
|
+
const write = (level, msg) => {
|
|
5110
|
+
try {
|
|
5111
|
+
appendFileSync2(
|
|
5112
|
+
target,
|
|
5113
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}
|
|
5114
|
+
`
|
|
5115
|
+
);
|
|
5116
|
+
} catch {
|
|
5117
|
+
}
|
|
5118
|
+
};
|
|
5119
|
+
return {
|
|
5120
|
+
debug: (m) => write("debug", m),
|
|
5121
|
+
info: (m) => write("info", m),
|
|
5122
|
+
warn: (m) => write("warn", m),
|
|
5123
|
+
error: (m) => write("error", m)
|
|
5124
|
+
};
|
|
5125
|
+
}
|
|
5126
|
+
var logger = createTuiLogger();
|
|
4661
5127
|
var SORT_MODES = [
|
|
4662
5128
|
{ key: "best-match", label: "best-match" },
|
|
4663
5129
|
{ key: "stars", label: "stars" },
|
|
@@ -5926,6 +6392,7 @@ async function launchBrowser(theme_override) {
|
|
|
5926
6392
|
if (!state.lastInstalledVersion) return;
|
|
5927
6393
|
if (state.lastInstalledVersion === currentVersion) return;
|
|
5928
6394
|
const notes = fetchReleaseNotes(currentVersion);
|
|
6395
|
+
markPostUpgradeSeen(currentVersion);
|
|
5929
6396
|
updateSelectedOption = 0;
|
|
5930
6397
|
updateHeader.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion}
|
|
5931
6398
|
`;
|
|
@@ -6522,16 +6989,13 @@ async function launchBrowser(theme_override) {
|
|
|
6522
6989
|
renderer.requestRender();
|
|
6523
6990
|
try {
|
|
6524
6991
|
const searchModule = new SearchModule(new TrendingAdapter());
|
|
6525
|
-
const response = await searchModule.search(
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
trendingSince: tabSince(trendingTab)
|
|
6533
|
-
}
|
|
6534
|
-
);
|
|
6992
|
+
const response = await searchModule.search(trendingQuery(), {
|
|
6993
|
+
limit: 25,
|
|
6994
|
+
sort: "stars",
|
|
6995
|
+
json: false,
|
|
6996
|
+
verbose: false,
|
|
6997
|
+
trendingSince: tabSince(trendingTab)
|
|
6998
|
+
});
|
|
6535
6999
|
const fetched = response.repos;
|
|
6536
7000
|
const since = tabSince(trendingTab);
|
|
6537
7001
|
trendingPeriod = since === "daily" ? "today" : since === "weekly" ? "this week" : "this month";
|
|
@@ -6671,7 +7135,10 @@ async function launchBrowser(theme_override) {
|
|
|
6671
7135
|
currentRepos = newRepos;
|
|
6672
7136
|
}
|
|
6673
7137
|
if (currentRepos.length === 0) {
|
|
6674
|
-
throw new NoResultsError(
|
|
7138
|
+
throw new NoResultsError(
|
|
7139
|
+
q,
|
|
7140
|
+
suggestRelaxations(parsed).map((r) => r.suggestion)
|
|
7141
|
+
);
|
|
6675
7142
|
} else {
|
|
6676
7143
|
resultsSelect.options = currentRepos.map((r) => ({
|
|
6677
7144
|
name: formatResultLine(r),
|
|
@@ -7663,7 +8130,7 @@ function formatPackageLines(pkgs) {
|
|
|
7663
8130
|
function asMcpToolError(err) {
|
|
7664
8131
|
return new McpToolError(err instanceof Error ? err.message : String(err));
|
|
7665
8132
|
}
|
|
7666
|
-
var TRENDING_QUERY =
|
|
8133
|
+
var TRENDING_QUERY = trendingQuery();
|
|
7667
8134
|
var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
|
|
7668
8135
|
var SKILL_SOURCES = ["local", "registry"];
|
|
7669
8136
|
var TRENDING_SINCE = ["daily", "weekly", "monthly"];
|
|
@@ -7710,6 +8177,17 @@ var TOOLS = [
|
|
|
7710
8177
|
const token = resolveToken(state, args.token);
|
|
7711
8178
|
const provider = createGitHubSearch(void 0, token ? [token] : []);
|
|
7712
8179
|
const parsed = parseQuery(query);
|
|
8180
|
+
try {
|
|
8181
|
+
validateQuery(parsed);
|
|
8182
|
+
} catch (err) {
|
|
8183
|
+
if (err instanceof BadQueryError) {
|
|
8184
|
+
return {
|
|
8185
|
+
text: `Invalid query: ${err.detail}${err.hint ? ` Try: ${err.hint}` : ""}`,
|
|
8186
|
+
data: { totalCount: 0, repos: [], invalidQuery: true }
|
|
8187
|
+
};
|
|
8188
|
+
}
|
|
8189
|
+
throw err;
|
|
8190
|
+
}
|
|
7713
8191
|
const options = {
|
|
7714
8192
|
limit,
|
|
7715
8193
|
sort,
|
|
@@ -8667,6 +9145,7 @@ Options:
|
|
|
8667
9145
|
}
|
|
8668
9146
|
function buildSearchContext(flags) {
|
|
8669
9147
|
const parsed = applyFlagFilters(parseQuery(flags.query), {});
|
|
9148
|
+
validateQuery(parsed);
|
|
8670
9149
|
return {
|
|
8671
9150
|
query: flags.query,
|
|
8672
9151
|
parsed,
|
|
@@ -9098,17 +9577,14 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
9098
9577
|
console.error(err instanceof Error ? err.message : String(err));
|
|
9099
9578
|
process.exit(1);
|
|
9100
9579
|
}
|
|
9101
|
-
const response = await trendingSearch.search(
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
...language ? { trendingLanguage: language } : {}
|
|
9110
|
-
}
|
|
9111
|
-
);
|
|
9580
|
+
const response = await trendingSearch.search(trendingQuery(), {
|
|
9581
|
+
limit: 25,
|
|
9582
|
+
sort: "stars",
|
|
9583
|
+
json: false,
|
|
9584
|
+
verbose: false,
|
|
9585
|
+
trendingSince: since,
|
|
9586
|
+
...language ? { trendingLanguage: language } : {}
|
|
9587
|
+
});
|
|
9112
9588
|
if (outputFormat) {
|
|
9113
9589
|
console.log(format(response.repos, outputFormat));
|
|
9114
9590
|
} else if (flags.count) {
|