github-search-cli 9.8.2 → 9.8.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/dist/cli.js CHANGED
@@ -16,6 +16,20 @@ var NetworkError = class extends SearchCliError {
16
16
  this.name = "NetworkError";
17
17
  }
18
18
  };
19
+ var AuthError = class extends SearchCliError {
20
+ constructor() {
21
+ const msg = "\u26A0 Bad GitHub token (401). Press [t] to fix it, [u] to search without one";
22
+ super("GitHub API returned 401 (invalid or revoked token)", msg);
23
+ this.name = "AuthError";
24
+ }
25
+ };
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);
30
+ this.name = "ForbiddenError";
31
+ }
32
+ };
19
33
  var RateLimitError = class extends SearchCliError {
20
34
  constructor(hasToken, resetSeconds) {
21
35
  const msg = hasToken ? `\u26A0 API rate limit exceeded (5,000/hr). Resets in ${resetSeconds ?? "?"}s. [r]etry [c]hange token` : "\u26A0 Rate limited (60/hr). Set GITHUB_TOKEN for 5,000/hr. [r]etry";
@@ -430,7 +444,14 @@ var GitHubSearchAdapter = class {
430
444
  this.logger.error(
431
445
  `[github] non-rate-limit 403: ${body.slice(0, 200)}`
432
446
  );
433
- throw new NetworkError();
447
+ throw new ForbiddenError();
448
+ }
449
+ if (res.status === 401) {
450
+ const body = await res.text().catch(() => "");
451
+ this.logger.error(
452
+ `[github] auth rejected (401): ${body.slice(0, 200)}`
453
+ );
454
+ throw new AuthError();
434
455
  }
435
456
  if (!res.ok) {
436
457
  const body = await res.text().catch(() => "");
@@ -467,6 +488,7 @@ var GitHubSearchAdapter = class {
467
488
  lastErr = void 0;
468
489
  break;
469
490
  } catch (err) {
491
+ if (err instanceof RateLimitError) throw err;
470
492
  lastErr = err;
471
493
  if (rateLimited) break;
472
494
  }
@@ -1715,7 +1737,7 @@ var cached = null;
1715
1737
  function getVersion() {
1716
1738
  if (cached) return cached;
1717
1739
  if (true) {
1718
- cached = "9.8.2";
1740
+ cached = "9.8.3";
1719
1741
  return cached;
1720
1742
  }
1721
1743
  try {
@@ -2865,6 +2887,122 @@ function comparisonMarkdown(repos) {
2865
2887
  return [header, sep, ...rows].join("\n");
2866
2888
  }
2867
2889
 
2890
+ // src/markdown-images.ts
2891
+ var MIN_IMAGE_PIXELS = 100;
2892
+ var IMG_TAG = /<img\b[^>]*?src\s*=\s*["']([^"']+)["'][^>]*>/gi;
2893
+ var ALT_ATTR = /\balt\s*=\s*["']([^"']*)["']/i;
2894
+ var LENGTH_ATTR = /\b(width|height)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
2895
+ var PIXEL_VALUE = /^(\d+)(?:px)?$/i;
2896
+ var HTML_TAG = /<\/?[a-z][^>]*>/gi;
2897
+ function declaredImageSize(tag) {
2898
+ const values = {};
2899
+ for (const [, name, dq, sq] of tag.matchAll(LENGTH_ATTR)) {
2900
+ const raw = dq ?? sq ?? "";
2901
+ const match = PIXEL_VALUE.exec(raw.trim());
2902
+ if (!match) continue;
2903
+ const n = Number.parseInt(match[1], 10);
2904
+ if (Number.isFinite(n) && n > 0) {
2905
+ values[name] = n;
2906
+ }
2907
+ }
2908
+ if (values.width === void 0 && values.height === void 0)
2909
+ return void 0;
2910
+ return {
2911
+ width: values.width ?? values.height ?? 0,
2912
+ height: values.height ?? values.width ?? 0
2913
+ };
2914
+ }
2915
+ function declaresTooSmall(size) {
2916
+ if (!size) return false;
2917
+ return size.width < MIN_IMAGE_PIXELS || size.height < MIN_IMAGE_PIXELS;
2918
+ }
2919
+ function imagesFromHtml(raw) {
2920
+ if (!raw || !/<img\b/i.test(raw)) return [];
2921
+ const withoutTags = raw.replace(IMG_TAG, "").replace(/<\/?(?:p|div|center|br|a|picture|source)\b[^>]*>/gi, "").trim();
2922
+ if (withoutTags) return [];
2923
+ const refs = [];
2924
+ for (const match of raw.matchAll(IMG_TAG)) {
2925
+ const tag = match[0];
2926
+ const alt = ALT_ATTR.exec(tag)?.[1] ?? "";
2927
+ refs.push({ href: match[1], alt, declared: declaredImageSize(tag) });
2928
+ }
2929
+ return refs;
2930
+ }
2931
+ function inlinePlainText(tokens) {
2932
+ if (!tokens) return "";
2933
+ let out = "";
2934
+ for (const token of tokens) {
2935
+ if (token.type === "image") continue;
2936
+ if (token.type === "html") {
2937
+ out += (token.raw ?? "").replace(HTML_TAG, "");
2938
+ continue;
2939
+ }
2940
+ if (token.type === "br") {
2941
+ out += "\n";
2942
+ continue;
2943
+ }
2944
+ if (token.text && (!token.tokens || token.tokens.length === 0)) {
2945
+ out += token.text;
2946
+ continue;
2947
+ }
2948
+ if (token.tokens?.length) {
2949
+ out += inlinePlainText(token.tokens);
2950
+ continue;
2951
+ }
2952
+ if (token.raw) out += token.raw.replace(HTML_TAG, "");
2953
+ }
2954
+ return out;
2955
+ }
2956
+ function extractImageRefs(token) {
2957
+ const images = [];
2958
+ let linkUrl;
2959
+ const collect = (inline2) => {
2960
+ if (inline2.type === "image") {
2961
+ if (inline2.href)
2962
+ images.push({ href: inline2.href, alt: inline2.text ?? "", linkUrl });
2963
+ return true;
2964
+ }
2965
+ if (inline2.type === "link" || inline2.type === "linkReference") {
2966
+ if (inline2.tokens?.length) {
2967
+ const outer = linkUrl;
2968
+ linkUrl = inline2.href || outer;
2969
+ const ok = inline2.tokens.every(collect);
2970
+ linkUrl = outer;
2971
+ return ok;
2972
+ }
2973
+ return false;
2974
+ }
2975
+ if (inline2.type === "html") {
2976
+ const refs = imagesFromHtml(inline2.raw);
2977
+ if (refs.length > 0) {
2978
+ images.push(...refs);
2979
+ return true;
2980
+ }
2981
+ return !(inline2.raw ?? "").replace(HTML_TAG, "").trim();
2982
+ }
2983
+ if (inline2.type === "text" || inline2.type === "escape") {
2984
+ return !(inline2.text ?? "").trim();
2985
+ }
2986
+ return false;
2987
+ };
2988
+ if (token.type === "image" && token.href) {
2989
+ return [{ href: token.href, alt: token.text ?? "" }];
2990
+ }
2991
+ if (token.type === "html") {
2992
+ const refs = imagesFromHtml(token.raw);
2993
+ return refs.length > 0 ? dropTooSmall(refs) : null;
2994
+ }
2995
+ const inline = token.tokens ?? [];
2996
+ if (inline.length === 0) return null;
2997
+ for (const child of inline) {
2998
+ if (!collect(child)) return null;
2999
+ }
3000
+ return images.length > 0 ? dropTooSmall(images) : null;
3001
+ }
3002
+ function dropTooSmall(refs) {
3003
+ return refs.filter((image) => !declaresTooSmall(image.declared));
3004
+ }
3005
+
2868
3006
  // src/image-loader.ts
2869
3007
  var MAX_IMAGE_BYTES = 6 * 1024 * 1024;
2870
3008
  var BADGE_HOST = /(^|\.)(shields\.io|badgen\.net|badge\.fury\.io|codecov\.io|coveralls\.io|travis-ci\.(org|com)|snyk\.io|sonarcloud\.io|david-dm\.(org|io))$/i;
@@ -3032,6 +3170,23 @@ async function loadImageBytes(url, opts) {
3032
3170
  function readmeBaseUrl(owner, repo, branch, path = "README.md") {
3033
3171
  return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
3034
3172
  }
3173
+ async function imageSizeFromBytes(bytes) {
3174
+ const ImageCtor = globalThis.Bun?.Image;
3175
+ if (!ImageCtor) return void 0;
3176
+ try {
3177
+ const meta = await new ImageCtor(bytes).metadata();
3178
+ if (!meta.width || !meta.height) return void 0;
3179
+ return { width: meta.width, height: meta.height };
3180
+ } catch {
3181
+ return void 0;
3182
+ }
3183
+ }
3184
+ async function shouldRenderImage(bytes, declared) {
3185
+ if (declared) return !declaresTooSmall(declared);
3186
+ const measured = await imageSizeFromBytes(bytes);
3187
+ if (!measured) return true;
3188
+ return !declaresTooSmall(measured);
3189
+ }
3035
3190
 
3036
3191
  // src/readme.ts
3037
3192
  var CANDIDATES = [
@@ -3340,6 +3495,17 @@ function buildHelpSections() {
3340
3495
  { keys: "PageUp", action: "Scroll to top of results" }
3341
3496
  ]
3342
3497
  },
3498
+ {
3499
+ title: " Error Recovery ",
3500
+ titleColor: "red",
3501
+ rows: [
3502
+ { keys: "r", action: "Retry after an error" },
3503
+ { keys: "t", action: "Fix GitHub token (401/403)" },
3504
+ { keys: "u", action: "Search without a token (401)" },
3505
+ { keys: "c", action: "Change token (rate limit)" }
3506
+ ],
3507
+ note: "Active only while the matching error is in the status bar."
3508
+ },
3343
3509
  {
3344
3510
  title: " Trending Mode ",
3345
3511
  titleColor: "yellow",
@@ -3552,9 +3718,35 @@ function touchSavedSearch(name) {
3552
3718
 
3553
3719
  // src/session.ts
3554
3720
  var SESSION_FILE = "session.json";
3721
+ var MAX_QUERY_LENGTH = 200;
3722
+ function sanitizeQuery(query) {
3723
+ if (typeof query !== "string") return void 0;
3724
+ const trimmed = query.trim();
3725
+ if (trimmed.length === 0 || trimmed.length > MAX_QUERY_LENGTH) {
3726
+ return void 0;
3727
+ }
3728
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
3729
+ return void 0;
3730
+ }
3731
+ return trimmed;
3732
+ }
3733
+ function sanitizeSession(state) {
3734
+ if (!state || typeof state !== "object") return null;
3735
+ const mode = state.mode === "trending" || state.mode === "packages" || state.mode === "search" ? state.mode : "search";
3736
+ const trendingTab = typeof state.trendingTab === "string" && state.trendingTab.length > 0 ? state.trendingTab : "This Week";
3737
+ const limit = typeof state.limit === "number" && Number.isFinite(state.limit) ? state.limit : 50;
3738
+ const sort = typeof state.sort === "string" ? state.sort : "best-match";
3739
+ return {
3740
+ mode,
3741
+ query: sanitizeQuery(state.query) ?? "",
3742
+ sort,
3743
+ limit,
3744
+ trendingTab
3745
+ };
3746
+ }
3555
3747
  function saveSession(state) {
3556
3748
  try {
3557
- writeJSON(SESSION_FILE, state);
3749
+ writeJSON(SESSION_FILE, sanitizeSession(state));
3558
3750
  } catch (err) {
3559
3751
  if (process.env.DEBUG)
3560
3752
  console.error(
@@ -3563,7 +3755,7 @@ function saveSession(state) {
3563
3755
  }
3564
3756
  }
3565
3757
  function restoreSession() {
3566
- return readJSON(SESSION_FILE, null);
3758
+ return sanitizeSession(readJSON(SESSION_FILE, null));
3567
3759
  }
3568
3760
 
3569
3761
  // src/markdown-view.ts
@@ -3869,83 +4061,6 @@ async function rasterizeImage(bytes, opts) {
3869
4061
  }
3870
4062
  }
3871
4063
 
3872
- // src/markdown-images.ts
3873
- var IMG_TAG = /<img\b[^>]*?src\s*=\s*["']([^"']+)["'][^>]*>/gi;
3874
- var ALT_ATTR = /\balt\s*=\s*["']([^"']*)["']/i;
3875
- var HTML_TAG = /<\/?[a-z][^>]*>/gi;
3876
- function imagesFromHtml(raw) {
3877
- if (!raw || !/<img\b/i.test(raw)) return [];
3878
- const withoutTags = raw.replace(IMG_TAG, "").replace(/<\/?(?:p|div|center|br|a|picture|source)\b[^>]*>/gi, "").trim();
3879
- if (withoutTags) return [];
3880
- const refs = [];
3881
- for (const match of raw.matchAll(IMG_TAG)) {
3882
- const tag = match[0];
3883
- const alt = ALT_ATTR.exec(tag)?.[1] ?? "";
3884
- refs.push({ href: match[1], alt });
3885
- }
3886
- return refs;
3887
- }
3888
- function inlinePlainText(tokens) {
3889
- if (!tokens) return "";
3890
- let out = "";
3891
- for (const token of tokens) {
3892
- if (token.type === "image") continue;
3893
- if (token.type === "html") {
3894
- out += (token.raw ?? "").replace(HTML_TAG, "");
3895
- continue;
3896
- }
3897
- if (token.type === "br") {
3898
- out += "\n";
3899
- continue;
3900
- }
3901
- if (token.text && (!token.tokens || token.tokens.length === 0)) {
3902
- out += token.text;
3903
- continue;
3904
- }
3905
- if (token.tokens?.length) {
3906
- out += inlinePlainText(token.tokens);
3907
- continue;
3908
- }
3909
- if (token.raw) out += token.raw.replace(HTML_TAG, "");
3910
- }
3911
- return out;
3912
- }
3913
- function extractImageRefs(token) {
3914
- const images = [];
3915
- const collect = (inline2) => {
3916
- if (inline2.type === "image") {
3917
- if (inline2.href)
3918
- images.push({ href: inline2.href, alt: inline2.text ?? "" });
3919
- return true;
3920
- }
3921
- if (inline2.type === "html") {
3922
- const refs = imagesFromHtml(inline2.raw);
3923
- if (refs.length > 0) {
3924
- images.push(...refs);
3925
- return true;
3926
- }
3927
- return !(inline2.raw ?? "").replace(HTML_TAG, "").trim();
3928
- }
3929
- if (inline2.type === "text" || inline2.type === "escape") {
3930
- return !(inline2.text ?? "").trim();
3931
- }
3932
- return false;
3933
- };
3934
- if (token.type === "image" && token.href) {
3935
- return [{ href: token.href, alt: token.text ?? "" }];
3936
- }
3937
- if (token.type === "html") {
3938
- const refs = imagesFromHtml(token.raw);
3939
- return refs.length > 0 ? refs : null;
3940
- }
3941
- const inline = token.tokens ?? [];
3942
- if (inline.length === 0) return null;
3943
- for (const child of inline) {
3944
- if (!collect(child)) return null;
3945
- }
3946
- return images.length > 0 ? images : null;
3947
- }
3948
-
3949
4064
  // src/markdown-view.ts
3950
4065
  var MAX_IMAGE_ROWS = 24;
3951
4066
  var RASTER_CACHE_LIMIT = 32;
@@ -3981,17 +4096,19 @@ function hexToRgb(hex) {
3981
4096
  function rgba(color) {
3982
4097
  return RGBA.fromValues(color[0], color[1], color[2], 255);
3983
4098
  }
3984
- function imageStyledText(image) {
4099
+ function imageStyledText(image, linkUrl) {
3985
4100
  const chunks = [];
3986
4101
  toImageRuns(image).forEach((runs, rowIndex) => {
3987
4102
  if (rowIndex > 0) chunks.push({ __isChunk: true, text: "\n" });
3988
4103
  for (const run2 of runs) {
3989
- chunks.push({
4104
+ const chunk = {
3990
4105
  __isChunk: true,
3991
4106
  text: run2.text,
3992
4107
  fg: rgba(run2.top),
3993
4108
  bg: rgba(run2.bottom)
3994
- });
4109
+ };
4110
+ if (linkUrl) chunk.link = { url: linkUrl };
4111
+ chunks.push(chunk);
3995
4112
  }
3996
4113
  });
3997
4114
  return new StyledText3(chunks);
@@ -4018,15 +4135,21 @@ function createMarkdownView(opts) {
4018
4135
  tableOptions: { style: "grid" },
4019
4136
  width: "100%"
4020
4137
  });
4021
- const caption = (label, color = muted()) => new TextRenderable3(renderer, {
4022
- content: ` ${label}`,
4023
- fg: color,
4138
+ const caption = (label, linkUrl, color = muted()) => new TextRenderable3(renderer, {
4139
+ content: new StyledText3([
4140
+ {
4141
+ __isChunk: true,
4142
+ text: ` ${label}`,
4143
+ fg: RGBA.fromHex(color),
4144
+ ...linkUrl ? { link: { url: linkUrl } } : {}
4145
+ }
4146
+ ]),
4024
4147
  bg: bg(),
4025
4148
  wrapMode: "word",
4026
4149
  flexShrink: 0,
4027
4150
  width: "100%"
4028
4151
  });
4029
- const rasterize = (url) => {
4152
+ const rasterize = (url, declared) => {
4030
4153
  const maxCols = Math.max(8, Math.floor(opts.getImageWidth?.() ?? 100));
4031
4154
  const key = `${maxCols}|${url}`;
4032
4155
  const cached2 = rasterCache.get(key);
@@ -4036,6 +4159,7 @@ function createMarkdownView(opts) {
4036
4159
  if (!loader) return null;
4037
4160
  const bytes = await loader(url);
4038
4161
  if (!bytes) return null;
4162
+ if (!await shouldRenderImage(bytes, declared)) return null;
4039
4163
  return rasterizeImage(bytes, {
4040
4164
  maxCols,
4041
4165
  maxRows: maxImageRows,
@@ -4053,24 +4177,25 @@ function createMarkdownView(opts) {
4053
4177
  const imageNode = (image) => {
4054
4178
  const label = `\u{1F5BC} ${image.alt.trim() || "image"}`;
4055
4179
  const url = imageBase ? resolveReadmeImageUrl(image.href, imageBase) : null;
4056
- if (!imagesEnabled || !url || !opts.loadImage) return caption(label);
4180
+ if (!imagesEnabled || !url || !opts.loadImage)
4181
+ return caption(label, image.linkUrl);
4057
4182
  const container = new BoxRenderable3(renderer, {
4058
4183
  flexDirection: "column",
4059
4184
  flexShrink: 0,
4060
4185
  width: "100%"
4061
4186
  });
4062
- const placeholder = caption(`${label} (loading\u2026)`);
4187
+ const placeholder = caption(`${label} (loading\u2026)`, image.linkUrl);
4063
4188
  container.add(placeholder);
4064
- void rasterize(url).then(
4189
+ void rasterize(url, image.declared).then(
4065
4190
  (cells) => {
4066
4191
  if (container.isDestroyed || !container.parent) return;
4067
4192
  container.remove(placeholder);
4068
4193
  placeholder.destroy();
4069
- container.add(caption(label));
4194
+ container.add(caption(label, image.linkUrl));
4070
4195
  if (cells) {
4071
4196
  container.add(
4072
4197
  new TextRenderable3(renderer, {
4073
- content: imageStyledText(cells),
4198
+ content: imageStyledText(cells, image.linkUrl),
4074
4199
  bg: bg(),
4075
4200
  wrapMode: "none",
4076
4201
  selectable: false,
@@ -4112,17 +4237,22 @@ function createMarkdownView(opts) {
4112
4237
  const buildRenderNode = () => (token, _context) => {
4113
4238
  if (token.type === "heading") return headingNode(token);
4114
4239
  const images = extractImageRefs(token);
4115
- if (images && images.length > 0) {
4116
- const box = new BoxRenderable3(renderer, {
4117
- flexDirection: "column",
4240
+ if (images === null) return void 0;
4241
+ if (images.length === 0) {
4242
+ return new BoxRenderable3(renderer, {
4118
4243
  flexShrink: 0,
4119
4244
  width: "100%",
4120
- marginTop: 1
4245
+ height: 0
4121
4246
  });
4122
- for (const image of images) box.add(imageNode(image));
4123
- return box;
4124
4247
  }
4125
- return void 0;
4248
+ const box = new BoxRenderable3(renderer, {
4249
+ flexDirection: "column",
4250
+ flexShrink: 0,
4251
+ width: "100%",
4252
+ marginTop: 1
4253
+ });
4254
+ for (const image of images) box.add(imageNode(image));
4255
+ return box;
4126
4256
  };
4127
4257
  renderable.renderNode = buildRenderNode();
4128
4258
  return {
@@ -4572,7 +4702,7 @@ async function launchBrowser(theme_override) {
4572
4702
  const root = renderer.root;
4573
4703
  root.flexDirection = "column";
4574
4704
  const config = loadConfig();
4575
- const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
4705
+ let githubToken = config.githubToken || process.env.GITHUB_TOKEN;
4576
4706
  const theme = loadTheme(theme_override || config.theme);
4577
4707
  Object.assign(colors, theme);
4578
4708
  if (terminalBg) {
@@ -4755,6 +4885,10 @@ async function launchBrowser(theme_override) {
4755
4885
  currentMode = "search";
4756
4886
  showLanding(false);
4757
4887
  showSearchMode();
4888
+ if (currentQueryInput) {
4889
+ searchInput.value = currentQueryInput;
4890
+ renderer.requestRender();
4891
+ }
4758
4892
  }
4759
4893
  },
4760
4894
  {
@@ -6079,9 +6213,85 @@ async function launchBrowser(theme_override) {
6079
6213
  setStatus(nextTip());
6080
6214
  }
6081
6215
  }, 2e3);
6216
+ const tokenPromptBox = new BoxRenderable5(renderer, {
6217
+ visible: false,
6218
+ position: "absolute",
6219
+ top: 0,
6220
+ left: 0,
6221
+ width: "100%",
6222
+ flexDirection: "row",
6223
+ paddingY: 1,
6224
+ paddingLeft: 1,
6225
+ backgroundColor: colors.surface
6226
+ });
6227
+ const tokenPromptLabel = new TextRenderable5(renderer, {
6228
+ content: " GitHub token:",
6229
+ fg: colors.accent,
6230
+ height: 1
6231
+ });
6232
+ const tokenPromptInput = new InputRenderable(renderer, {
6233
+ placeholder: "paste token, Enter to save \u2014 Esc cancels",
6234
+ value: "",
6235
+ backgroundColor: colors.surface,
6236
+ textColor: colors.text,
6237
+ flexGrow: 1
6238
+ });
6239
+ tokenPromptBox.add(tokenPromptLabel);
6240
+ tokenPromptBox.add(tokenPromptInput);
6241
+ root.add(tokenPromptBox);
6242
+ let tokenPromptQuery = null;
6243
+ function showTokenPrompt(query) {
6244
+ tokenPromptQuery = query;
6245
+ tokenPromptInput.value = "";
6246
+ tokenPromptBox.visible = true;
6247
+ searchBox.visible = false;
6248
+ tokenPromptInput.focus();
6249
+ renderer.requestRender();
6250
+ }
6251
+ function hideTokenPrompt() {
6252
+ tokenPromptBox.visible = false;
6253
+ tokenPromptQuery = null;
6254
+ if (currentMode === "search" || currentMode === "packages") {
6255
+ searchBox.visible = true;
6256
+ searchInput.focus();
6257
+ }
6258
+ renderer.requestRender();
6259
+ }
6260
+ function persistToken2(token) {
6261
+ githubToken = token;
6262
+ config.githubToken = token;
6263
+ saveConfig(config);
6264
+ }
6265
+ tokenPromptInput.on("enter", () => {
6266
+ const token = tokenPromptInput.value.trim();
6267
+ const query = tokenPromptQuery;
6268
+ hideTokenPrompt();
6269
+ if (!token || !query) return;
6270
+ persistToken2(token);
6271
+ setStatus("Token saved");
6272
+ doSearch(query);
6273
+ });
6274
+ function fixTokenAction() {
6275
+ showTokenPrompt(currentQueryInput || "stars:>10000");
6276
+ }
6277
+ function unsetTokenAction() {
6278
+ githubToken = void 0;
6279
+ if (config.githubToken) {
6280
+ delete config.githubToken;
6281
+ saveConfig(config);
6282
+ }
6283
+ setStatus("Token cleared \u2014 searching without one");
6284
+ if (currentQueryInput) doSearch(currentQueryInput);
6285
+ }
6082
6286
  function setStatus(msg) {
6083
6287
  statusMgr.set("idle", msg);
6084
6288
  }
6289
+ let errorHint = null;
6290
+ function reportError2(err) {
6291
+ errorHint = err instanceof AuthError ? "auth" : err instanceof ForbiddenError ? "forbidden" : err instanceof RateLimitError ? "ratelimit" : err instanceof NetworkError ? "network" : null;
6292
+ const msg = err instanceof SearchCliError ? err.userMessage : err instanceof Error ? err.message : String(err);
6293
+ statusMgr.set("error", msg.slice(0, 60));
6294
+ }
6085
6295
  function buildSessionState() {
6086
6296
  const mode = currentMode === "trending" ? "trending" : currentMode === "packages" ? "packages" : "search";
6087
6297
  return {
@@ -6298,6 +6508,7 @@ async function launchBrowser(theme_override) {
6298
6508
  async function loadTrending() {
6299
6509
  currentMode = "trending";
6300
6510
  isLoading = true;
6511
+ errorHint = null;
6301
6512
  searchBox.visible = false;
6302
6513
  toolbarText.visible = false;
6303
6514
  trendingTabBox.visible = true;
@@ -6347,11 +6558,10 @@ async function launchBrowser(theme_override) {
6347
6558
  resultCount: fetched.length
6348
6559
  });
6349
6560
  } catch (err) {
6350
- const msg = err instanceof SearchCliError ? err.userMessage : err instanceof Error ? err.message : String(err);
6351
6561
  resultsSelect.options = [
6352
6562
  { name: " (error)", description: "", value: null }
6353
6563
  ];
6354
- statusMgr.set("error", msg.slice(0, 60));
6564
+ reportError2(err);
6355
6565
  }
6356
6566
  isLoading = false;
6357
6567
  renderer.requestRender();
@@ -6390,6 +6600,7 @@ async function launchBrowser(theme_override) {
6390
6600
  if (q === "" || isLoading) return;
6391
6601
  currentQueryInput = q;
6392
6602
  isLoading = true;
6603
+ errorHint = null;
6393
6604
  resultsSelect.options = [
6394
6605
  { name: " Searching packages...", description: "", value: null }
6395
6606
  ];
@@ -6427,6 +6638,7 @@ async function launchBrowser(theme_override) {
6427
6638
  async function doSearch(queryText, append = false) {
6428
6639
  const q = queryText.trim();
6429
6640
  if (q === "" || isLoading) return;
6641
+ errorHint = null;
6430
6642
  if (!append) currentPage = 1;
6431
6643
  deepDiveActive = false;
6432
6644
  isLoading = true;
@@ -6489,8 +6701,7 @@ async function launchBrowser(theme_override) {
6489
6701
  { name: " (error)", description: "", value: null }
6490
6702
  ];
6491
6703
  }
6492
- const msg = err instanceof SearchCliError ? err.userMessage : err instanceof Error ? err.message : String(err);
6493
- statusMgr.set("error", msg.slice(0, 60));
6704
+ reportError2(err);
6494
6705
  }
6495
6706
  isLoading = false;
6496
6707
  renderer.requestRender();
@@ -6587,6 +6798,16 @@ ${pack.description ?? ""}`;
6587
6798
  }
6588
6799
  });
6589
6800
  renderer.keyInput.on("keypress", (key) => {
6801
+ if (tokenPromptBox.visible) {
6802
+ if (key.name === "escape") {
6803
+ hideTokenPrompt();
6804
+ setStatus("Token entry cancelled");
6805
+ renderer.requestRender();
6806
+ return;
6807
+ }
6808
+ if (key.name === "enter" || key.name === "return") return;
6809
+ return;
6810
+ }
6590
6811
  if (currentOverlay !== "none") {
6591
6812
  if (key.name === "escape") {
6592
6813
  if (currentOverlay === "leader") {
@@ -6890,6 +7111,27 @@ ${pack.description ?? ""}`;
6890
7111
  showLeaderMenu();
6891
7112
  return;
6892
7113
  }
7114
+ if (!searchInput.focused && errorHint) {
7115
+ if (key.name === "r") {
7116
+ errorHint = null;
7117
+ if (currentMode === "trending") loadTrending();
7118
+ else if (currentQueryInput) doSearch(currentQueryInput);
7119
+ renderer.requestRender();
7120
+ return;
7121
+ }
7122
+ if (key.name === "t" && (errorHint === "auth" || errorHint === "forbidden")) {
7123
+ fixTokenAction();
7124
+ return;
7125
+ }
7126
+ if (key.name === "u" && errorHint === "auth") {
7127
+ unsetTokenAction();
7128
+ return;
7129
+ }
7130
+ if (key.name === "c" && errorHint === "ratelimit") {
7131
+ fixTokenAction();
7132
+ return;
7133
+ }
7134
+ }
6893
7135
  if (key.name === "t" && !searchInput.focused) {
6894
7136
  const current = config.theme;
6895
7137
  const themes = listThemes();
@@ -6984,25 +7226,10 @@ ${pack.description ?? ""}`;
6984
7226
  }
6985
7227
  });
6986
7228
  const _currentVersion = getVersion();
6987
- if (!session?.mode) {
6988
- showLanding(true);
6989
- }
7229
+ showLanding(true);
6990
7230
  checkForUpdateAndShow();
6991
7231
  checkPostUpgradePanel();
6992
7232
  renderer.start();
6993
- if (session?.mode) {
6994
- if (currentMode === "trending") {
6995
- loadTrending();
6996
- } else {
6997
- searchInput.value = currentQueryInput;
6998
- if (currentQueryInput) {
6999
- doSearch(currentQueryInput);
7000
- } else {
7001
- showSearchMode();
7002
- }
7003
- searchInput.focus();
7004
- }
7005
- }
7006
7233
  renderer.requestRender();
7007
7234
  }
7008
7235
  function formatResultLine(repo) {
@@ -7490,7 +7717,18 @@ var TOOLS = [
7490
7717
  verbose: false,
7491
7718
  token
7492
7719
  };
7493
- const response = await provider.search(parsed, options);
7720
+ let response;
7721
+ try {
7722
+ response = await provider.search(parsed, options);
7723
+ } catch (err) {
7724
+ if (err instanceof RateLimitError) {
7725
+ return {
7726
+ text: `GitHub API rate limit exceeded for "${query}". Set a token (GITHUB_TOKEN or the tool's token argument) to raise the limit from 60/hr to 5,000/hr.`,
7727
+ data: { totalCount: 0, repos: [], rateLimited: true }
7728
+ };
7729
+ }
7730
+ throw err;
7731
+ }
7494
7732
  const repos = rankRepos(response.repos, sort);
7495
7733
  try {
7496
7734
  appendHistory({