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/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 = "\u26A0 Forbidden (403). If your org enforces SSO, authorize the token; [t] to change it";
29
- super("GitHub API returned 403 (token lacks access)", msg);
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
- constructor(query) {
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}". Tips: check spelling, try fewer qualifiers, or [b]rowse trending`
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 matches = KNOWN_QUALIFIERS.filter((k) => k.startsWith(trimmed));
247
- return matches.map((k) => `${k}:`);
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
- qualifiers.push(qualifier);
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 (value === "") return null;
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 Error("Cannot combine `visibility:` and `private:` filters.");
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: ${body.slice(0, 200)}`
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
- throw new ForbiddenError();
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.8.3";
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
- { keywords: [], qualifiers: [], raw: "trending" },
2007
- {
2008
- limit: opts.limit,
2009
- sort: opts.sort,
2010
- json: false,
2011
- verbose: false,
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.split(".").map((s) => Number(s.split("-")[0]));
4463
- const newParts = latest.split(".").map((s) => Number(s.split("-")[0]));
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) return true;
4468
- if (n < c) return false;
4898
+ if (n > c) {
4899
+ order = 1;
4900
+ break;
4901
+ }
4902
+ if (n < c) {
4903
+ order = -1;
4904
+ break;
4905
+ }
4469
4906
  }
4470
- return false;
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
- var logger = {
4652
- debug: () => {
4653
- },
4654
- info: () => {
4655
- },
4656
- warn: () => {
4657
- },
4658
- error: () => {
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
- { keywords: [], qualifiers: [], raw: "trending" },
6527
- {
6528
- limit: 25,
6529
- sort: "stars",
6530
- json: false,
6531
- verbose: false,
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(q);
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 = { keywords: [], qualifiers: [], raw: "trending" };
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
- { keywords: [], qualifiers: [], raw: "trending" },
9103
- {
9104
- limit: 25,
9105
- sort: "stars",
9106
- json: false,
9107
- verbose: false,
9108
- trendingSince: since,
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) {