github-search-cli 9.8.1 → 9.8.2

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
@@ -1,21 +1,11 @@
1
- import {
2
- cacheDir,
3
- configPath,
4
- loadConfig,
5
- saveConfig,
6
- stateDir
7
- } from "./chunk-GYIL4S5X.js";
8
-
9
1
  // src/errors.ts
10
2
  var SearchCliError = class extends Error {
11
- constructor(message, userMessage, recoverable = true) {
3
+ constructor(message, userMessage) {
12
4
  super(message);
13
5
  this.userMessage = userMessage;
14
- this.recoverable = recoverable;
15
6
  this.name = "SearchCliError";
16
7
  }
17
8
  userMessage;
18
- recoverable;
19
9
  };
20
10
  var NetworkError = class extends SearchCliError {
21
11
  constructor() {
@@ -154,6 +144,7 @@ var MemoryCache = class {
154
144
  return entry.data;
155
145
  }
156
146
  set(key, data, ttlMs) {
147
+ this.cache.delete(key);
157
148
  if (this.cache.size >= MAX_CACHE_SIZE) {
158
149
  const firstKey = this.cache.keys().next().value;
159
150
  if (firstKey !== void 0) this.cache.delete(firstKey);
@@ -1190,13 +1181,199 @@ async function launchPackageBrowser() {
1190
1181
  }
1191
1182
 
1192
1183
  // src/doctor.ts
1193
- import { existsSync, readFileSync, mkdirSync } from "fs";
1184
+ import { existsSync as existsSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
1194
1185
  import { spawnSync } from "child_process";
1195
- import { join, dirname } from "path";
1186
+ import { join as join3, dirname } from "path";
1196
1187
  import { fileURLToPath } from "url";
1197
- import { homedir, platform, arch } from "os";
1188
+ import { homedir as homedir3, platform, arch } from "os";
1189
+
1190
+ // src/config.ts
1191
+ import { homedir } from "os";
1192
+ import { join } from "path";
1193
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
1194
+ function stateDir() {
1195
+ const xdg = process.env.XDG_STATE_HOME;
1196
+ const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".local", "share", "ghfind");
1197
+ return base;
1198
+ }
1199
+ function cacheDir() {
1200
+ const xdg = process.env.XDG_CACHE_HOME;
1201
+ const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".cache", "ghfind");
1202
+ return base;
1203
+ }
1204
+ var DEFAULTS = {
1205
+ defaultSort: "best-match",
1206
+ defaultLimit: 50,
1207
+ theme: "tokyo-night",
1208
+ cacheTtlSeconds: 300,
1209
+ defaultTab: "search"
1210
+ };
1211
+ function configPath() {
1212
+ const envPath = process.env.GHFIND_CONFIG;
1213
+ if (envPath) return envPath;
1214
+ const xdg = process.env.XDG_CONFIG_HOME;
1215
+ const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".config", "ghfind");
1216
+ return join(base, "config.json");
1217
+ }
1218
+ function loadConfig() {
1219
+ const path = configPath();
1220
+ try {
1221
+ const raw = readFileSync(path, "utf-8");
1222
+ const parsed = JSON.parse(raw);
1223
+ return mergeWithEnv({ ...DEFAULTS, ...parsed });
1224
+ } catch {
1225
+ return mergeWithEnv({ ...DEFAULTS });
1226
+ }
1227
+ }
1228
+ function saveConfig(config) {
1229
+ const path = configPath();
1230
+ const dir = path.slice(
1231
+ 0,
1232
+ Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
1233
+ );
1234
+ mkdirSync(dir, { recursive: true });
1235
+ writeFileSync(path, JSON.stringify(config, null, 2), {
1236
+ encoding: "utf-8",
1237
+ mode: 384
1238
+ });
1239
+ try {
1240
+ chmodSync(path, 384);
1241
+ } catch {
1242
+ }
1243
+ }
1244
+ function mergeWithEnv(config) {
1245
+ if (process.env.GITHUB_TOKEN) config.githubToken = process.env.GITHUB_TOKEN;
1246
+ return config;
1247
+ }
1248
+
1249
+ // src/login.ts
1250
+ import { createInterface } from "readline";
1251
+ import { homedir as homedir2 } from "os";
1252
+ import { join as join2 } from "path";
1253
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1254
+ var TOKEN_PREFIXES = ["ghp_", "gho_", "github_pat_"];
1255
+ function isLikelyGithubToken(token) {
1256
+ return TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix));
1257
+ }
1258
+ function maskToken(token) {
1259
+ if (token.length <= 11) return `${token.slice(0, 4)}\u2026`;
1260
+ return `${token.slice(0, 7)}\u2026${token.slice(-4)}`;
1261
+ }
1262
+ function ghCliConfigDir() {
1263
+ return process.env.GH_CONFIG_DIR || join2(homedir2(), ".config", "gh");
1264
+ }
1265
+ function parseHostsYmlToken(text, host = "github.com") {
1266
+ let inHost = false;
1267
+ for (const line of text.split(/\r?\n/)) {
1268
+ const trimmed = line.trim();
1269
+ if (/^[A-Za-z0-9.-]+:\s*$/.test(trimmed)) {
1270
+ inHost = trimmed.slice(0, -1).trim() === host;
1271
+ continue;
1272
+ }
1273
+ if (inHost) {
1274
+ const match = trimmed.match(/^oauth_token:\s*["']?([^"'\s]+)/);
1275
+ if (match) return match[1];
1276
+ }
1277
+ }
1278
+ return void 0;
1279
+ }
1280
+ function readGhCliToken(ghDir = ghCliConfigDir(), host = process.env.GH_HOST ?? "github.com") {
1281
+ const hostsFile = join2(ghDir, "hosts.yml");
1282
+ if (!existsSync(hostsFile)) return void 0;
1283
+ try {
1284
+ return parseHostsYmlToken(readFileSync2(hostsFile, "utf-8"), host);
1285
+ } catch {
1286
+ return void 0;
1287
+ }
1288
+ }
1289
+ function persistToken(token, deps) {
1290
+ const config = deps.readConfig();
1291
+ config.githubToken = token;
1292
+ deps.writeConfig(config);
1293
+ }
1294
+ function createAsk() {
1295
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1296
+ return (query) => new Promise((resolve) => {
1297
+ rl.question(query, (answer) => {
1298
+ rl.close();
1299
+ resolve(answer.trim());
1300
+ });
1301
+ });
1302
+ }
1303
+ function defaultLoginDeps() {
1304
+ return {
1305
+ ask: createAsk(),
1306
+ readGhCliToken: () => readGhCliToken(),
1307
+ readConfig: loadConfig,
1308
+ writeConfig: (config) => saveConfig(config)
1309
+ };
1310
+ }
1311
+ function successNote() {
1312
+ return [
1313
+ " Authentication raises the search rate limit from 60 to",
1314
+ " 5,000 requests/hour. Remove the token anytime by editing",
1315
+ ` ${configPath()} \u2014 delete the "githubToken" line and save.`
1316
+ ];
1317
+ }
1318
+ async function runLoginWizard(deps = defaultLoginDeps()) {
1319
+ const { ask: ask2 } = deps;
1320
+ console.log("");
1321
+ console.log(" ghfind login");
1322
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1323
+ console.log("");
1324
+ if (process.env.GITHUB_TOKEN) {
1325
+ console.log(
1326
+ " \u2139 GITHUB_TOKEN is set in the environment and already takes precedence."
1327
+ );
1328
+ console.log("");
1329
+ }
1330
+ const current = deps.readConfig();
1331
+ if (current.githubToken) {
1332
+ console.log(
1333
+ ` \u2713 A token is already saved: ${maskToken(current.githubToken)}`
1334
+ );
1335
+ }
1336
+ const ghToken = deps.readGhCliToken();
1337
+ if (ghToken) {
1338
+ console.log(" \u2713 Found a token managed by the GitHub CLI (gh).");
1339
+ const answer = await ask2(" Import it into ghfind? [Y/n]: ");
1340
+ if (answer.toLowerCase() !== "n") {
1341
+ persistToken(ghToken, deps);
1342
+ finishLogin(ghToken);
1343
+ return;
1344
+ }
1345
+ }
1346
+ const pasted = await ask2(" Paste a GitHub token (Enter to skip): ");
1347
+ if (!pasted) {
1348
+ console.log(" Skipped \u2014 no token saved.");
1349
+ console.log("");
1350
+ return;
1351
+ }
1352
+ if (!isLikelyGithubToken(pasted)) {
1353
+ console.log(
1354
+ ` \u26A0 That doesn't look like a GitHub token (expected one of: ${TOKEN_PREFIXES.join(", ")}).`
1355
+ );
1356
+ const confirm = await ask2(" Save it anyway? [y/N]: ");
1357
+ if (confirm.toLowerCase() !== "y") {
1358
+ console.log(" Cancelled \u2014 nothing saved.");
1359
+ console.log("");
1360
+ return;
1361
+ }
1362
+ }
1363
+ persistToken(pasted, deps);
1364
+ finishLogin(pasted);
1365
+ }
1366
+ function finishLogin(token) {
1367
+ console.log(` \u2713 Token saved to ${configPath()}`);
1368
+ console.log(` (masked: ${maskToken(token)})`);
1369
+ console.log("");
1370
+ for (const line of successNote()) console.log(line);
1371
+ console.log("");
1372
+ }
1373
+
1374
+ // src/doctor.ts
1198
1375
  var __dirname = dirname(fileURLToPath(import.meta.url));
1199
- var PKG_DIR = join(__dirname, "..");
1376
+ var PKG_DIR = join3(__dirname, "..");
1200
1377
  var results = [];
1201
1378
  function add(status, label, detail) {
1202
1379
  results.push({ status, label, detail });
@@ -1222,7 +1399,7 @@ function hasSystemBun() {
1222
1399
  return res.status === 0 ? { ok: true, version: res.stdout.trim() } : { ok: false };
1223
1400
  }
1224
1401
  function bundledBunPath() {
1225
- return join(PKG_DIR, "vendor", platform() === "win32" ? "bun.exe" : "bun");
1402
+ return join3(PKG_DIR, "vendor", platform() === "win32" ? "bun.exe" : "bun");
1226
1403
  }
1227
1404
  function bunRuns(bin) {
1228
1405
  const res = run(bin, ["--version"]);
@@ -1232,7 +1409,7 @@ function bunRuns(bin) {
1232
1409
  function detectLibc() {
1233
1410
  if (platform() !== "linux") return "unknown";
1234
1411
  try {
1235
- const osRelease = readFileSync("/etc/os-release", "utf8");
1412
+ const osRelease = readFileSync3("/etc/os-release", "utf8");
1236
1413
  if (/alpine|musl/i.test(osRelease)) return "musl";
1237
1414
  } catch {
1238
1415
  }
@@ -1253,7 +1430,7 @@ async function checkRuntime() {
1253
1430
  }
1254
1431
  function checkBundledBun() {
1255
1432
  const path = bundledBunPath();
1256
- if (!existsSync(path)) {
1433
+ if (!existsSync2(path)) {
1257
1434
  add(
1258
1435
  "WARN",
1259
1436
  "Bundled Bun",
@@ -1302,8 +1479,8 @@ function checkNodeVersion() {
1302
1479
  }
1303
1480
  }
1304
1481
  function checkBuildOutput() {
1305
- const dist = join(PKG_DIR, "dist", "cli.js");
1306
- if (existsSync(dist)) {
1482
+ const dist = join3(PKG_DIR, "dist", "cli.js");
1483
+ if (existsSync2(dist)) {
1307
1484
  add("PASS", "Build output", `${dist}`);
1308
1485
  } else {
1309
1486
  add(
@@ -1314,8 +1491,8 @@ function checkBuildOutput() {
1314
1491
  }
1315
1492
  }
1316
1493
  function checkSource() {
1317
- const src = join(PKG_DIR, "src", "cli.ts");
1318
- if (existsSync(src)) {
1494
+ const src = join3(PKG_DIR, "src", "cli.ts");
1495
+ if (existsSync2(src)) {
1319
1496
  add("PASS", "Source", `${src}`);
1320
1497
  } else {
1321
1498
  add("FAIL", "Source", "src/cli.ts not found. Reinstall the package.");
@@ -1331,7 +1508,7 @@ function checkTuiBackend() {
1331
1508
  }
1332
1509
  function checkConfig() {
1333
1510
  const path = configPath();
1334
- if (!existsSync(path)) {
1511
+ if (!existsSync2(path)) {
1335
1512
  add(
1336
1513
  "WARN",
1337
1514
  "Config",
@@ -1340,7 +1517,7 @@ function checkConfig() {
1340
1517
  return;
1341
1518
  }
1342
1519
  try {
1343
- const raw = readFileSync(path, "utf-8");
1520
+ const raw = readFileSync3(path, "utf-8");
1344
1521
  JSON.parse(raw);
1345
1522
  add("PASS", "Config", `${path} (valid JSON)`);
1346
1523
  } catch (err) {
@@ -1355,8 +1532,8 @@ function checkGithubToken() {
1355
1532
  const config = loadConfig();
1356
1533
  const token = config.githubToken || process.env.GITHUB_TOKEN;
1357
1534
  if (token) {
1358
- const preview = token.slice(0, 7);
1359
- add("PASS", "GitHub token", `Configured (${preview}\u2026).`);
1535
+ const preview = maskToken(token);
1536
+ add("PASS", "GitHub token", `Configured (${preview}).`);
1360
1537
  } else {
1361
1538
  add(
1362
1539
  "WARN",
@@ -1371,7 +1548,7 @@ function checkDirs() {
1371
1548
  ["Cache dir", cacheDir()]
1372
1549
  ]) {
1373
1550
  try {
1374
- mkdirSync(dir, { recursive: true });
1551
+ mkdirSync2(dir, { recursive: true });
1375
1552
  add("PASS", label, dir);
1376
1553
  } catch (err) {
1377
1554
  add(
@@ -1387,7 +1564,7 @@ var BIN_NAME = "ghfind";
1387
1564
  function runningVersion() {
1388
1565
  try {
1389
1566
  const pkg = JSON.parse(
1390
- readFileSync(join(PKG_DIR, "package.json"), "utf8")
1567
+ readFileSync3(join3(PKG_DIR, "package.json"), "utf8")
1391
1568
  );
1392
1569
  return pkg.version ?? "";
1393
1570
  } catch {
@@ -1397,11 +1574,11 @@ function runningVersion() {
1397
1574
  function installVersionAt(shimPath) {
1398
1575
  const dir = dirname(shimPath);
1399
1576
  for (const ancestor of [dir, dirname(dir)]) {
1400
- for (const sub of ["node_modules", join("lib", "node_modules")]) {
1401
- const pkgJson = join(ancestor, sub, PKG_NAME, "package.json");
1402
- if (existsSync(pkgJson)) {
1577
+ for (const sub of ["node_modules", join3("lib", "node_modules")]) {
1578
+ const pkgJson = join3(ancestor, sub, PKG_NAME, "package.json");
1579
+ if (existsSync2(pkgJson)) {
1403
1580
  try {
1404
- const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
1581
+ const pkg = JSON.parse(readFileSync3(pkgJson, "utf8"));
1405
1582
  return { version: pkg.version ?? "", dir: dirname(pkgJson) };
1406
1583
  } catch {
1407
1584
  }
@@ -1481,7 +1658,7 @@ function printReport() {
1481
1658
  return `${code}${s}\x1B[0m`;
1482
1659
  };
1483
1660
  console.log("\n ghfind doctor \u2014 environment diagnostics");
1484
- console.log(` ${platform()} ${arch} \xB7 ${homedir()}`);
1661
+ console.log(` ${platform()} ${arch} \xB7 ${homedir3()}`);
1485
1662
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1486
1663
  for (const r of results) {
1487
1664
  console.log(
@@ -1531,20 +1708,20 @@ import {
1531
1708
  import { spawn as spawn2 } from "child_process";
1532
1709
 
1533
1710
  // src/version.ts
1534
- import { readFileSync as readFileSync2 } from "fs";
1535
- import { join as join2, dirname as dirname2 } from "path";
1711
+ import { readFileSync as readFileSync4 } from "fs";
1712
+ import { join as join4, dirname as dirname2 } from "path";
1536
1713
  import { fileURLToPath as fileURLToPath2 } from "url";
1537
1714
  var cached = null;
1538
1715
  function getVersion() {
1539
1716
  if (cached) return cached;
1540
1717
  if (true) {
1541
- cached = "9.8.1";
1718
+ cached = "9.8.2";
1542
1719
  return cached;
1543
1720
  }
1544
1721
  try {
1545
1722
  const here = dirname2(fileURLToPath2(import.meta.url));
1546
1723
  const pkg = JSON.parse(
1547
- readFileSync2(join2(here, "..", "package.json"), "utf-8")
1724
+ readFileSync4(join4(here, "..", "package.json"), "utf-8")
1548
1725
  );
1549
1726
  cached = pkg.version ?? "unknown";
1550
1727
  } catch {
@@ -1556,6 +1733,45 @@ function getVersion() {
1556
1733
  // src/error-report.ts
1557
1734
  var REPO = "codersdfs/search-cli";
1558
1735
  var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
1736
+ var SECRET_PLACEHOLDER = "***";
1737
+ var SECRET_FLAGS = /^(?:--token|-t)$/;
1738
+ var SECRET_PATTERNS = [
1739
+ /gh[pousr]_[A-Za-z0-9]{16,}/g,
1740
+ /github_pat_[A-Za-z0-9_]{20,}/g
1741
+ ];
1742
+ function redactSecrets(text) {
1743
+ let out = text;
1744
+ for (const pattern of SECRET_PATTERNS) {
1745
+ out = out.replace(pattern, SECRET_PLACEHOLDER);
1746
+ }
1747
+ return out;
1748
+ }
1749
+ function redactArgv(argv) {
1750
+ const out = [];
1751
+ let redactNext = false;
1752
+ for (const arg of argv) {
1753
+ if (redactNext) {
1754
+ out.push(SECRET_PLACEHOLDER);
1755
+ redactNext = false;
1756
+ continue;
1757
+ }
1758
+ if (SECRET_FLAGS.test(arg)) {
1759
+ out.push(arg);
1760
+ redactNext = true;
1761
+ continue;
1762
+ }
1763
+ const eq = arg.indexOf("=");
1764
+ if (eq > 0 && SECRET_FLAGS.test(arg.slice(0, eq))) {
1765
+ out.push(`${arg.slice(0, eq)}=${SECRET_PLACEHOLDER}`);
1766
+ continue;
1767
+ }
1768
+ out.push(redactSecrets(arg));
1769
+ }
1770
+ return out;
1771
+ }
1772
+ function argvHasSecret(argv) {
1773
+ return redactArgv(argv).some((arg, i) => arg !== argv[i]);
1774
+ }
1559
1775
  function getPackageVersion() {
1560
1776
  return getVersion();
1561
1777
  }
@@ -1565,9 +1781,12 @@ function runtimeName() {
1565
1781
  }
1566
1782
  function formatErrorBody(err, context) {
1567
1783
  const e = err instanceof Error ? err : new Error(String(err));
1568
- const stack = (e.stack ?? "(no stack)").replace(/\u001b\[[0-9;]*m/g, "");
1569
- const message = (e.message || "(no message)").slice(0, 500);
1570
- const cmd = `${context.command} ${context.argv.join(" ")}`.trim();
1784
+ const stack = redactSecrets(
1785
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: strips ANSI color escapes from stack traces
1786
+ (e.stack ?? "(no stack)").replace(/\u001b\[[0-9;]*m/g, "")
1787
+ );
1788
+ const message = redactSecrets(e.message || "(no message)").slice(0, 500);
1789
+ const cmd = `${context.command} ${redactArgv(context.argv).join(" ")}`.trim();
1571
1790
  const ghfindVersion = getPackageVersion();
1572
1791
  return [
1573
1792
  "## What happened",
@@ -1601,7 +1820,7 @@ function formatErrorBody(err, context) {
1601
1820
  }
1602
1821
  function buildIssueUrl(err, context) {
1603
1822
  const e = err instanceof Error ? err : new Error(String(err));
1604
- const title = e.message.slice(0, 80).replace(/[\r\n]+/g, " ");
1823
+ const title = redactSecrets(e.message).slice(0, 80).replace(/[\r\n]+/g, " ");
1605
1824
  const body = formatErrorBody(err, context);
1606
1825
  const params = new URLSearchParams({
1607
1826
  title: `[bug] ${title || "Unhandled error"}`,
@@ -1633,15 +1852,25 @@ function reportError(err, context = {
1633
1852
  command: "ghfind",
1634
1853
  argv: process.argv.slice(2)
1635
1854
  }) {
1636
- const url = buildIssueUrl(err, context);
1855
+ const hadSecret = argvHasSecret(context.argv);
1856
+ const url = buildIssueUrl(err, {
1857
+ command: context.command,
1858
+ argv: redactArgv(context.argv)
1859
+ });
1637
1860
  console.error("");
1638
1861
  console.error(" \u26A0 ghfind encountered an unexpected error.");
1639
1862
  console.error("");
1640
- console.error(" Help us fix it by opening a bug report with the");
1641
- console.error(" details pre-filled. Your browser will open the new-issue");
1642
- console.error(" page; review and submit (or close the tab to skip).");
1863
+ if (hadSecret) {
1864
+ console.error(" A token was found in the command line and has been");
1865
+ console.error(" redacted. Your browser will NOT be opened \u2014 review the");
1866
+ console.error(" scrubbed URL below before sharing it anywhere.");
1867
+ } else {
1868
+ console.error(" Help us fix it by opening a bug report with the");
1869
+ console.error(" details pre-filled. Your browser will open the new-issue");
1870
+ console.error(" page; review and submit (or close the tab to skip).");
1871
+ }
1643
1872
  console.error("");
1644
- const opened = tryOpenUrl(url);
1873
+ const opened = hadSecret ? false : tryOpenUrl(url);
1645
1874
  if (opened) {
1646
1875
  console.error(` \u2192 Opened: ${url}`);
1647
1876
  } else {
@@ -1654,7 +1883,7 @@ function reportError(err, context = {
1654
1883
  }
1655
1884
 
1656
1885
  // src/output.ts
1657
- import { writeFileSync } from "fs";
1886
+ import { writeFileSync as writeFileSync2 } from "fs";
1658
1887
  var FORMATS = /* @__PURE__ */ new Map([
1659
1888
  // Export formats
1660
1889
  ["json", formatJson],
@@ -1722,7 +1951,7 @@ function exportToFile(repos, fmt) {
1722
1951
  const ext = fmt === "markdown" ? "md" : fmt;
1723
1952
  const ts = Date.now();
1724
1953
  const path = `./ghfind-export-${ts}.${ext}`;
1725
- writeFileSync(path, format(repos, fmt), "utf-8");
1954
+ writeFileSync2(path, format(repos, fmt), "utf-8");
1726
1955
  return path;
1727
1956
  }
1728
1957
  async function pipeExec(repos, target) {
@@ -1880,17 +2109,18 @@ async function fetchOrgProfile(org, options = {}) {
1880
2109
  if (orgPayload.public_repos > 0) {
1881
2110
  const perPage = Math.min(limit, 100);
1882
2111
  const maxPages = Math.ceil(limit / perPage);
1883
- for (let page = 1; page <= maxPages; page++) {
1884
- const url = `https://api.github.com/orgs/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
1885
- const res = await fetch(url, { headers: apiHeaders(token) });
1886
- if (!res.ok) {
1887
- break;
1888
- }
1889
- const items = await res.json();
1890
- if (!Array.isArray(items)) break;
1891
- repos.push(...items);
1892
- if (repos.length >= limit || items.length < perPage) break;
1893
- }
2112
+ const pages = await Promise.all(
2113
+ Array.from({ length: maxPages }, (_, i) => i + 1).map(async (page) => {
2114
+ const url = `https://api.github.com/orgs/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
2115
+ const res = await fetch(url, { headers: apiHeaders(token) });
2116
+ if (!res.ok) {
2117
+ return [];
2118
+ }
2119
+ const items = await res.json();
2120
+ return Array.isArray(items) ? items : [];
2121
+ })
2122
+ );
2123
+ repos.push(...pages.flat());
1894
2124
  }
1895
2125
  return normalizeOrg(orgPayload, repos.slice(0, limit));
1896
2126
  }
@@ -2094,17 +2324,18 @@ async function fetchUserProfile(user, options = {}) {
2094
2324
  if (userPayload.public_repos > 0) {
2095
2325
  const perPage = Math.min(limit, 100);
2096
2326
  const maxPages = Math.ceil(limit / perPage);
2097
- for (let page = 1; page <= maxPages; page++) {
2098
- const url = `https://api.github.com/users/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
2099
- const res = await fetch(url, { headers: apiHeaders2(token) });
2100
- if (!res.ok) {
2101
- break;
2102
- }
2103
- const items = await res.json();
2104
- if (!Array.isArray(items)) break;
2105
- repos.push(...items);
2106
- if (repos.length >= limit || items.length < perPage) break;
2107
- }
2327
+ const pages = await Promise.all(
2328
+ Array.from({ length: maxPages }, (_, i) => i + 1).map(async (page) => {
2329
+ const url = `https://api.github.com/users/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
2330
+ const res = await fetch(url, { headers: apiHeaders2(token) });
2331
+ if (!res.ok) {
2332
+ return [];
2333
+ }
2334
+ const items = await res.json();
2335
+ return Array.isArray(items) ? items : [];
2336
+ })
2337
+ );
2338
+ repos.push(...pages.flat());
2108
2339
  }
2109
2340
  return normalizeUser(userPayload, repos.slice(0, limit));
2110
2341
  }
@@ -2220,34 +2451,48 @@ function formatUserMarkdown(p) {
2220
2451
 
2221
2452
  // src/storage.ts
2222
2453
  import {
2223
- existsSync as existsSync2,
2224
- mkdirSync as mkdirSync2,
2225
- readFileSync as readFileSync3,
2226
- writeFileSync as writeFileSync2,
2454
+ mkdirSync as mkdirSync3,
2455
+ readFileSync as readFileSync5,
2456
+ writeFileSync as writeFileSync3,
2227
2457
  appendFileSync,
2228
- unlinkSync
2458
+ unlinkSync,
2459
+ renameSync
2229
2460
  } from "fs";
2230
- import { join as join3 } from "path";
2461
+ import { join as join5 } from "path";
2231
2462
  function ensureDir() {
2232
- mkdirSync2(stateDir(), { recursive: true });
2463
+ mkdirSync3(stateDir(), { recursive: true });
2233
2464
  }
2234
2465
  function readJSON(filename, fallback) {
2235
2466
  ensureDir();
2236
2467
  try {
2237
- return JSON.parse(readFileSync3(join3(stateDir(), filename), "utf-8"));
2468
+ return JSON.parse(readFileSync5(join5(stateDir(), filename), "utf-8"));
2238
2469
  } catch {
2239
2470
  return fallback;
2240
2471
  }
2241
2472
  }
2242
- function writeJSON(filename, data) {
2473
+ function writeFileAtomic(filename, data) {
2243
2474
  ensureDir();
2244
- writeFileSync2(join3(stateDir(), filename), JSON.stringify(data, null, 2));
2475
+ const target = join5(stateDir(), filename);
2476
+ const tmp = `${target}.tmp-${process.pid}`;
2477
+ try {
2478
+ writeFileSync3(tmp, data, "utf-8");
2479
+ renameSync(tmp, target);
2480
+ } catch (e) {
2481
+ try {
2482
+ unlinkSync(tmp);
2483
+ } catch {
2484
+ }
2485
+ throw e;
2486
+ }
2487
+ }
2488
+ function writeJSON(filename, data) {
2489
+ writeFileAtomic(filename, JSON.stringify(data, null, 2));
2245
2490
  }
2246
2491
  function appendJSONL(filename, obj) {
2247
2492
  ensureDir();
2248
2493
  try {
2249
2494
  appendFileSync(
2250
- join3(stateDir(), filename),
2495
+ join5(stateDir(), filename),
2251
2496
  `${JSON.stringify(obj)}
2252
2497
  `,
2253
2498
  "utf-8"
@@ -2258,7 +2503,7 @@ function appendJSONL(filename, obj) {
2258
2503
  function readJSONL(filename) {
2259
2504
  ensureDir();
2260
2505
  try {
2261
- const raw = readFileSync3(join3(stateDir(), filename), "utf-8").trim();
2506
+ const raw = readFileSync5(join5(stateDir(), filename), "utf-8").trim();
2262
2507
  if (!raw) return [];
2263
2508
  return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
2264
2509
  } catch {
@@ -2266,14 +2511,13 @@ function readJSONL(filename) {
2266
2511
  }
2267
2512
  }
2268
2513
  function writeRaw(filename, data) {
2269
- ensureDir();
2270
- writeFileSync2(join3(stateDir(), filename), data);
2514
+ writeFileAtomic(filename, data);
2271
2515
  }
2272
2516
  function debugLog(msg) {
2273
2517
  if (!process.env.DEBUG) return;
2274
2518
  try {
2275
2519
  appendFileSync(
2276
- join3(stateDir(), "ghfind-debug.log"),
2520
+ join5(stateDir(), "ghfind-debug.log"),
2277
2521
  `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
2278
2522
  `,
2279
2523
  "utf-8"
@@ -2330,9 +2574,12 @@ function markSeen(fullName) {
2330
2574
  // src/notifications.ts
2331
2575
  var nextId = 1;
2332
2576
  var notifs = [];
2577
+ var loaded = false;
2333
2578
  var MAX_NOTIFS = 50;
2334
2579
  var FILE = "notifications.json";
2335
- function load() {
2580
+ function ensureLoaded() {
2581
+ if (loaded) return;
2582
+ loaded = true;
2336
2583
  notifs = readJSON(FILE, []);
2337
2584
  nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
2338
2585
  }
@@ -2340,7 +2587,7 @@ function save() {
2340
2587
  writeJSON(FILE, notifs);
2341
2588
  }
2342
2589
  function addNotification(type, message) {
2343
- if (notifs.length === 0) load();
2590
+ ensureLoaded();
2344
2591
  const n = {
2345
2592
  id: nextId++,
2346
2593
  type,
@@ -2370,14 +2617,16 @@ function iconFor(type) {
2370
2617
  }
2371
2618
  }
2372
2619
  function getNotifications() {
2373
- if (notifs.length === 0) load();
2620
+ ensureLoaded();
2374
2621
  return [...notifs];
2375
2622
  }
2376
2623
  function dismissNotification(id) {
2624
+ ensureLoaded();
2377
2625
  notifs = notifs.filter((n) => n.id !== id);
2378
2626
  save();
2379
2627
  }
2380
2628
  function dismissAll() {
2629
+ ensureLoaded();
2381
2630
  notifs = [];
2382
2631
  save();
2383
2632
  }
@@ -2385,6 +2634,7 @@ function dismissAll() {
2385
2634
  // src/releases.ts
2386
2635
  var RELEASES_FILE = "release-cache.json";
2387
2636
  var PER_REPO = 5;
2637
+ var CONCURRENCY = 6;
2388
2638
  function loadCache() {
2389
2639
  return readJSON(RELEASES_FILE, {});
2390
2640
  }
@@ -2424,43 +2674,31 @@ async function checkReleases(opts = {}) {
2424
2674
  const config = loadConfig();
2425
2675
  const token = opts.token ?? config.githubToken ?? process.env.GITHUB_TOKEN;
2426
2676
  const results2 = [];
2427
- for (const bm of bookmarks) {
2677
+ const checkOne = async (fullName) => {
2428
2678
  try {
2429
- const prev = cache2[bm.repo.fullName];
2430
- const lastSeen = getLastSeen(bm.repo.fullName);
2679
+ const prev = cache2[fullName];
2680
+ const lastSeen = getLastSeen(fullName);
2431
2681
  const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
2432
2682
  const { status, etag, releases } = await fetchReleases(
2433
- bm.repo.fullName,
2683
+ fullName,
2434
2684
  prev?.etag,
2435
2685
  token
2436
2686
  );
2437
2687
  if (status === 304 && prev) {
2438
- results2.push({
2439
- fullName: bm.repo.fullName,
2440
- newReleases: prev.releases.filter(isNew)
2441
- });
2442
- continue;
2688
+ return { fullName, newReleases: prev.releases.filter(isNew) };
2443
2689
  }
2444
2690
  if (status !== 200 || !releases) {
2445
- results2.push({
2446
- fullName: bm.repo.fullName,
2447
- newReleases: [],
2448
- error: `HTTP ${status}`
2449
- });
2450
- continue;
2691
+ return { fullName, newReleases: [], error: `HTTP ${status}` };
2451
2692
  }
2452
- cache2[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
2453
- results2.push({
2454
- fullName: bm.repo.fullName,
2455
- newReleases: releases.filter(isNew)
2456
- });
2693
+ cache2[fullName] = { checkedAt: Date.now(), etag, releases };
2694
+ return { fullName, newReleases: releases.filter(isNew) };
2457
2695
  } catch (err) {
2458
- results2.push({
2459
- fullName: bm.repo.fullName,
2460
- newReleases: [],
2461
- error: err.message
2462
- });
2696
+ return { fullName, newReleases: [], error: err.message };
2463
2697
  }
2698
+ };
2699
+ for (let i = 0; i < bookmarks.length; i += CONCURRENCY) {
2700
+ const batch = bookmarks.slice(i, i + CONCURRENCY).map((bm) => checkOne(bm.repo.fullName));
2701
+ results2.push(...await Promise.all(batch));
2464
2702
  }
2465
2703
  saveCache(cache2);
2466
2704
  return results2;
@@ -2480,141 +2718,14 @@ async function checkAndNotify(opts = {}) {
2480
2718
  }
2481
2719
  function allCachedReleases() {
2482
2720
  const cache2 = loadCache();
2483
- const bookmarks = getBookmarks();
2484
- return Object.entries(cache2).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
2721
+ const bookmarked = new Set(getBookmarks().map((b) => b.repo.fullName));
2722
+ return Object.entries(cache2).filter(([fullName]) => bookmarked.has(fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
2485
2723
  (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
2486
2724
  );
2487
2725
  }
2488
2726
 
2489
2727
  // src/init.ts
2490
2728
  import { createInterface as createInterface2 } from "readline";
2491
-
2492
- // src/login.ts
2493
- import { createInterface } from "readline";
2494
- import { homedir as homedir2 } from "os";
2495
- import { join as join4 } from "path";
2496
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2497
- var TOKEN_PREFIXES = ["ghp_", "gho_", "github_pat_"];
2498
- function isLikelyGithubToken(token) {
2499
- return TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix));
2500
- }
2501
- function maskToken(token) {
2502
- if (token.length <= 11) return `${token.slice(0, 4)}\u2026`;
2503
- return `${token.slice(0, 7)}\u2026${token.slice(-4)}`;
2504
- }
2505
- function ghCliConfigDir() {
2506
- return process.env.GH_CONFIG_DIR || join4(homedir2(), ".config", "gh");
2507
- }
2508
- function parseHostsYmlToken(text, host = "github.com") {
2509
- let inHost = false;
2510
- for (const line of text.split(/\r?\n/)) {
2511
- const trimmed = line.trim();
2512
- if (/^[A-Za-z0-9.-]+:\s*$/.test(trimmed)) {
2513
- inHost = trimmed.slice(0, -1).trim() === host;
2514
- continue;
2515
- }
2516
- if (inHost) {
2517
- const match = trimmed.match(/^oauth_token:\s*["']?([^"'\s]+)/);
2518
- if (match) return match[1];
2519
- }
2520
- }
2521
- return void 0;
2522
- }
2523
- function readGhCliToken(ghDir = ghCliConfigDir(), host = process.env.GH_HOST ?? "github.com") {
2524
- const hostsFile = join4(ghDir, "hosts.yml");
2525
- if (!existsSync3(hostsFile)) return void 0;
2526
- try {
2527
- return parseHostsYmlToken(readFileSync4(hostsFile, "utf-8"), host);
2528
- } catch {
2529
- return void 0;
2530
- }
2531
- }
2532
- function persistToken(token, deps) {
2533
- const config = deps.readConfig();
2534
- config.githubToken = token;
2535
- deps.writeConfig(config);
2536
- }
2537
- function createAsk() {
2538
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2539
- return (query) => new Promise((resolve) => {
2540
- rl.question(query, (answer) => {
2541
- rl.close();
2542
- resolve(answer.trim());
2543
- });
2544
- });
2545
- }
2546
- function defaultLoginDeps() {
2547
- return {
2548
- ask: createAsk(),
2549
- readGhCliToken: () => readGhCliToken(),
2550
- readConfig: loadConfig,
2551
- writeConfig: (config) => saveConfig(config)
2552
- };
2553
- }
2554
- function successNote() {
2555
- return [
2556
- " Authentication raises the search rate limit from 60 to",
2557
- " 5,000 requests/hour. Remove the token anytime by editing",
2558
- ` ${configPath()} \u2014 delete the "githubToken" line and save.`
2559
- ];
2560
- }
2561
- async function runLoginWizard(deps = defaultLoginDeps()) {
2562
- const { ask: ask2 } = deps;
2563
- console.log("");
2564
- console.log(" ghfind login");
2565
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2566
- console.log("");
2567
- if (process.env.GITHUB_TOKEN) {
2568
- console.log(
2569
- " \u2139 GITHUB_TOKEN is set in the environment and already takes precedence."
2570
- );
2571
- console.log("");
2572
- }
2573
- const current = deps.readConfig();
2574
- if (current.githubToken) {
2575
- console.log(
2576
- ` \u2713 A token is already saved: ${maskToken(current.githubToken)}`
2577
- );
2578
- }
2579
- const ghToken = deps.readGhCliToken();
2580
- if (ghToken) {
2581
- console.log(" \u2713 Found a token managed by the GitHub CLI (gh).");
2582
- const answer = await ask2(" Import it into ghfind? [Y/n]: ");
2583
- if (answer.toLowerCase() !== "n") {
2584
- persistToken(ghToken, deps);
2585
- finishLogin(ghToken);
2586
- return;
2587
- }
2588
- }
2589
- const pasted = await ask2(" Paste a GitHub token (Enter to skip): ");
2590
- if (!pasted) {
2591
- console.log(" Skipped \u2014 no token saved.");
2592
- console.log("");
2593
- return;
2594
- }
2595
- if (!isLikelyGithubToken(pasted)) {
2596
- console.log(
2597
- ` \u26A0 That doesn't look like a GitHub token (expected one of: ${TOKEN_PREFIXES.join(", ")}).`
2598
- );
2599
- const confirm = await ask2(" Save it anyway? [y/N]: ");
2600
- if (confirm.toLowerCase() !== "y") {
2601
- console.log(" Cancelled \u2014 nothing saved.");
2602
- console.log("");
2603
- return;
2604
- }
2605
- }
2606
- persistToken(pasted, deps);
2607
- finishLogin(pasted);
2608
- }
2609
- function finishLogin(token) {
2610
- console.log(` \u2713 Token saved to ${configPath()}`);
2611
- console.log(` (masked: ${maskToken(token)})`);
2612
- console.log("");
2613
- for (const line of successNote()) console.log(line);
2614
- console.log("");
2615
- }
2616
-
2617
- // src/init.ts
2618
2729
  function ask(query) {
2619
2730
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
2620
2731
  return new Promise((resolve) => {
@@ -2650,8 +2761,7 @@ async function runInitWizard() {
2650
2761
  };
2651
2762
  if (token) config.githubToken = token;
2652
2763
  saveConfig(config);
2653
- const { configPath: configPath2 } = await import("./config-OCIPF2IF.js");
2654
- console.log(` \u2713 Config saved to ${configPath2()}`);
2764
+ console.log(` \u2713 Config saved to ${configPath()}`);
2655
2765
  console.log(" Run 'ghfind' to start browsing!");
2656
2766
  console.log("");
2657
2767
  }
@@ -2755,6 +2865,203 @@ function comparisonMarkdown(repos) {
2755
2865
  return [header, sep, ...rows].join("\n");
2756
2866
  }
2757
2867
 
2868
+ // src/image-loader.ts
2869
+ var MAX_IMAGE_BYTES = 6 * 1024 * 1024;
2870
+ 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;
2871
+ var RAW_HOSTS = /* @__PURE__ */ new Set([
2872
+ "raw.githubusercontent.com",
2873
+ "camo.githubusercontent.com",
2874
+ "user-images.githubusercontent.com",
2875
+ "private-user-images.githubusercontent.com",
2876
+ "avatars.githubusercontent.com",
2877
+ "objects.githubusercontent.com"
2878
+ ]);
2879
+ var RASTER_EXT = /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)(?:[?#]|$)/i;
2880
+ var CACHE_LIMIT = 24;
2881
+ var MAX_CONCURRENT_IMAGE_FETCHES = 4;
2882
+ var cache = /* @__PURE__ */ new Map();
2883
+ var activeFetches = 0;
2884
+ var waiting = [];
2885
+ function releaseSlot() {
2886
+ activeFetches--;
2887
+ waiting.shift()?.();
2888
+ }
2889
+ async function withSlot(job) {
2890
+ if (activeFetches >= MAX_CONCURRENT_IMAGE_FETCHES) {
2891
+ await new Promise((resolve) => waiting.push(resolve));
2892
+ }
2893
+ activeFetches++;
2894
+ try {
2895
+ return await job();
2896
+ } finally {
2897
+ releaseSlot();
2898
+ }
2899
+ }
2900
+ function decodeDataUri(uri) {
2901
+ const match = /^data:([^;,]+)?(;base64)?,(.*)$/is.exec(uri);
2902
+ if (!match) return null;
2903
+ const mime = (match[1] ?? "").toLowerCase();
2904
+ if (mime.includes("svg")) return null;
2905
+ const payload = match[3];
2906
+ try {
2907
+ if (match[2]) return new Uint8Array(Buffer.from(payload, "base64"));
2908
+ return new Uint8Array(Buffer.from(decodeURIComponent(payload), "binary"));
2909
+ } catch {
2910
+ return null;
2911
+ }
2912
+ }
2913
+ function toRawGitHubUrl(url) {
2914
+ if (url.hostname.toLowerCase() !== "github.com") return url;
2915
+ const match = /^\/([^/]+)\/([^/]+)\/(?:blob|raw)\/(.+)$/.exec(url.pathname);
2916
+ if (!match) return url;
2917
+ const [, owner, repo, rest] = match;
2918
+ return new URL(
2919
+ `https://raw.githubusercontent.com/${owner}/${repo}/${rest}${url.search}`
2920
+ );
2921
+ }
2922
+ function resolveRootRelative(path, base) {
2923
+ let parsed;
2924
+ try {
2925
+ parsed = new URL(base);
2926
+ } catch {
2927
+ return null;
2928
+ }
2929
+ if (parsed.hostname.toLowerCase() !== "raw.githubusercontent.com") {
2930
+ return null;
2931
+ }
2932
+ const segments = parsed.pathname.split("/").filter(Boolean);
2933
+ if (segments.length < 3) return null;
2934
+ return new URL(`${parsed.origin}/${segments.slice(0, 3).join("/")}${path}`);
2935
+ }
2936
+ function resolveReadmeImageUrl(raw, base) {
2937
+ const cleaned = raw.trim();
2938
+ if (!cleaned) return null;
2939
+ if (/^data:/i.test(cleaned)) {
2940
+ return isRenderableImageUrl(cleaned) ? cleaned : null;
2941
+ }
2942
+ let url = null;
2943
+ try {
2944
+ url = cleaned.startsWith("/") ? resolveRootRelative(cleaned, base) : new URL(cleaned, base);
2945
+ } catch {
2946
+ return null;
2947
+ }
2948
+ if (!url) return null;
2949
+ url = toRawGitHubUrl(url);
2950
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
2951
+ url.hash = "";
2952
+ const resolved = url.toString();
2953
+ return isRenderableImageUrl(resolved) ? resolved : null;
2954
+ }
2955
+ function isRenderableImageUrl(url) {
2956
+ if (/^data:/i.test(url)) return !/svg/i.test(url.slice(0, 64));
2957
+ let parsed;
2958
+ try {
2959
+ parsed = new URL(url);
2960
+ } catch {
2961
+ return false;
2962
+ }
2963
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
2964
+ if (BADGE_HOST.test(parsed.hostname)) return false;
2965
+ const path = parsed.pathname.toLowerCase();
2966
+ if (path.endsWith(".svg") || path.endsWith(".svgz")) return false;
2967
+ if (parsed.hostname.toLowerCase().endsWith("github.com")) {
2968
+ if (/^\/user-attachments\//.test(path)) return true;
2969
+ if (!RAW_HOSTS.has(parsed.hostname)) {
2970
+ return /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)$/i.test(path);
2971
+ }
2972
+ }
2973
+ if (/\.[a-z0-9]{1,5}$/.test(path) && !RASTER_EXT.test(path)) return false;
2974
+ return true;
2975
+ }
2976
+ function isGitHubHost(hostname) {
2977
+ return /(^|\.)githubusercontent\.com$|(^|\.)github\.com$/i.test(hostname);
2978
+ }
2979
+ async function fetchImageBytes(url, opts = {}) {
2980
+ const cached2 = cache.get(url);
2981
+ if (cached2) return cached2;
2982
+ const pending = withSlot(() => loadImageBytes(url, opts));
2983
+ cache.set(url, pending);
2984
+ while (cache.size > CACHE_LIMIT) {
2985
+ const oldest = cache.keys().next().value;
2986
+ if (oldest === void 0) break;
2987
+ cache.delete(oldest);
2988
+ }
2989
+ return pending;
2990
+ }
2991
+ async function loadImageBytes(url, opts) {
2992
+ if (/^data:/i.test(url)) return decodeDataUri(url);
2993
+ const fetchImpl = opts.fetchImpl ?? fetch;
2994
+ const timeoutMs = opts.timeoutMs ?? 1e4;
2995
+ const controller = new AbortController();
2996
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2997
+ try {
2998
+ const headers = {
2999
+ "User-Agent": "ghfind/1.0",
3000
+ Accept: "image/*,*/*;q=0.5"
3001
+ };
3002
+ let host = "";
3003
+ try {
3004
+ host = new URL(url).hostname;
3005
+ } catch {
3006
+ return null;
3007
+ }
3008
+ if (opts.token && isGitHubHost(host)) {
3009
+ headers.Authorization = `Bearer ${opts.token}`;
3010
+ }
3011
+ const res = await fetchImpl(url, { headers, signal: controller.signal });
3012
+ if (!res.ok) return null;
3013
+ const contentType = res.headers.get("content-type") ?? "";
3014
+ if (contentType && !/^image\//i.test(contentType)) {
3015
+ const githubHost = isGitHubHost(host);
3016
+ if (!githubHost || !/octet-stream/i.test(contentType)) return null;
3017
+ }
3018
+ if (/svg/i.test(contentType)) return null;
3019
+ const declared = Number(res.headers.get("content-length") ?? "0");
3020
+ if (declared > MAX_IMAGE_BYTES) return null;
3021
+ const buffer = new Uint8Array(await res.arrayBuffer());
3022
+ if (buffer.byteLength === 0 || buffer.byteLength > MAX_IMAGE_BYTES) {
3023
+ return null;
3024
+ }
3025
+ return buffer;
3026
+ } catch {
3027
+ return null;
3028
+ } finally {
3029
+ clearTimeout(timer);
3030
+ }
3031
+ }
3032
+ function readmeBaseUrl(owner, repo, branch, path = "README.md") {
3033
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
3034
+ }
3035
+
3036
+ // src/readme.ts
3037
+ var CANDIDATES = [
3038
+ ["main", "README.md"],
3039
+ ["master", "README.md"],
3040
+ ["main", "README.rst"]
3041
+ ];
3042
+ function buildReadmeHeaders(url, token, isPrivate = false) {
3043
+ const headers = { "User-Agent": "ghfind/1.0" };
3044
+ if (token && isPrivate && new URL(url).origin === "https://api.github.com") {
3045
+ headers.Authorization = `Bearer ${token}`;
3046
+ }
3047
+ return headers;
3048
+ }
3049
+ async function fetchReadme(owner, repo, opts = {}) {
3050
+ const fetchImpl = opts.fetchImpl ?? fetch;
3051
+ const attempts = CANDIDATES.map(async ([branch, file]) => {
3052
+ const url = readmeBaseUrl(owner, repo, branch, file);
3053
+ const headers = buildReadmeHeaders(url, opts.token, opts.private);
3054
+ const res = await fetchImpl(url, { headers });
3055
+ if (!res.ok) throw new Error(`no readme at ${url}`);
3056
+ return { text: await res.text(), sourceUrl: url };
3057
+ });
3058
+ try {
3059
+ return await Promise.any(attempts);
3060
+ } catch {
3061
+ return null;
3062
+ }
3063
+ }
3064
+
2758
3065
  // src/deepdive.ts
2759
3066
  var USER_AGENT = "ghfind/1.0";
2760
3067
  function buildHeaders(token) {
@@ -2799,42 +3106,46 @@ function repoFromRef(fullName) {
2799
3106
  async function resolveRepoFromRef(fullName, token) {
2800
3107
  const ref = parseRepoRef(fullName);
2801
3108
  const canonical = `${ref.owner}/${ref.name}`;
3109
+ let res;
2802
3110
  try {
2803
- const res = await fetch(`https://api.github.com/repos/${canonical}`, {
3111
+ res = await fetch(`https://api.github.com/repos/${canonical}`, {
2804
3112
  headers: buildHeaders(token)
2805
3113
  });
2806
- if (res.status === 404) {
2807
- throw new Error(`Repo not found: ${canonical}`);
2808
- }
2809
- if (res.ok) {
2810
- const api = await res.json();
2811
- return {
2812
- id: 0,
2813
- fullName: canonical,
2814
- name: ref.name,
2815
- owner: ref.owner,
2816
- description: api.description,
2817
- url: api.html_url,
2818
- stars: api.stargazers_count ?? 0,
2819
- forks: api.forks_count ?? 0,
2820
- watchers: api.subscribers_count ?? 0,
2821
- language: api.language,
2822
- topics: api.topics ?? [],
2823
- archived: api.archived ?? false,
2824
- isFork: api.fork ?? false,
2825
- private: false,
2826
- createdAt: api.created_at ?? "",
2827
- updatedAt: api.updated_at ?? "",
2828
- pushedAt: api.pushed_at ?? "",
2829
- score: 0
2830
- };
2831
- }
2832
- } catch (err) {
2833
- if (err instanceof Error && err.message.startsWith("Repo not found")) {
2834
- throw err;
2835
- }
3114
+ } catch {
3115
+ return repoFromRef(canonical);
3116
+ }
3117
+ if (res.status === 404) {
3118
+ throw new Error(`Repo not found: ${canonical}`);
3119
+ }
3120
+ if (!res.ok) {
3121
+ throw new Error(`GitHub API error ${res.status} for ${canonical}`);
3122
+ }
3123
+ let api;
3124
+ try {
3125
+ api = await res.json();
3126
+ } catch {
3127
+ return repoFromRef(canonical);
2836
3128
  }
2837
- return repoFromRef(canonical);
3129
+ return {
3130
+ id: 0,
3131
+ fullName: canonical,
3132
+ name: ref.name,
3133
+ owner: ref.owner,
3134
+ description: api.description,
3135
+ url: api.html_url,
3136
+ stars: api.stargazers_count ?? 0,
3137
+ forks: api.forks_count ?? 0,
3138
+ watchers: api.subscribers_count ?? 0,
3139
+ language: api.language,
3140
+ topics: api.topics ?? [],
3141
+ archived: api.archived ?? false,
3142
+ isFork: api.fork ?? false,
3143
+ private: api.private ?? false,
3144
+ createdAt: api.created_at ?? "",
3145
+ updatedAt: api.updated_at ?? "",
3146
+ pushedAt: api.pushed_at ?? "",
3147
+ score: 0
3148
+ };
2838
3149
  }
2839
3150
  var README_MAX_CHARS = 5e3;
2840
3151
  async function fetchDeepDiveRaw(repo, token) {
@@ -2848,20 +3159,11 @@ async function fetchDeepDiveRaw(repo, token) {
2848
3159
  { headers }
2849
3160
  )
2850
3161
  ]);
2851
- let readmeText = "";
2852
- for (const path of [
2853
- `${repo.owner}/${repo.name}/main/README.md`,
2854
- `${repo.owner}/${repo.name}/master/README.md`,
2855
- `${repo.owner}/${repo.name}/main/README.rst`
2856
- ]) {
2857
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
2858
- headers
2859
- });
2860
- if (r.ok) {
2861
- readmeText = await r.text();
2862
- break;
2863
- }
2864
- }
3162
+ const readme = await fetchReadme(repo.owner, repo.name, {
3163
+ token: repo.private ? token : void 0,
3164
+ private: repo.private
3165
+ });
3166
+ const readmeText = readme?.text ?? "";
2865
3167
  const langEntries = languagesRes.ok ? Object.entries(
2866
3168
  await languagesRes.json().catch(() => ({}))
2867
3169
  ) : [];
@@ -2952,8 +3254,8 @@ function buildDeepDiveText(data) {
2952
3254
  }
2953
3255
 
2954
3256
  // src/cli.ts
2955
- import { readFileSync as readFileSync7 } from "fs";
2956
- import { join as join7, dirname as dirname4 } from "path";
3257
+ import { readFileSync as readFileSync8 } from "fs";
3258
+ import { join as join8, dirname as dirname4 } from "path";
2957
3259
  import { fileURLToPath as fileURLToPath4 } from "url";
2958
3260
 
2959
3261
  // src/tui.ts
@@ -3176,13 +3478,10 @@ function readHistory() {
3176
3478
  function deleteHistoryEntry(index) {
3177
3479
  const entries = readHistory();
3178
3480
  if (index < 0 || index >= entries.length) return;
3179
- const removed = entries[index];
3180
- const remaining = entries.filter(
3181
- (e) => e.timestamp !== removed.timestamp || e.query !== removed.query
3182
- );
3481
+ entries.splice(index, 1);
3183
3482
  writeRaw(
3184
3483
  HISTORY_FILE,
3185
- `${remaining.reverse().map((e) => JSON.stringify(e)).join("\n")}
3484
+ `${entries.reverse().map((e) => JSON.stringify(e)).join("\n")}
3186
3485
  `
3187
3486
  );
3188
3487
  }
@@ -3570,174 +3869,6 @@ async function rasterizeImage(bytes, opts) {
3570
3869
  }
3571
3870
  }
3572
3871
 
3573
- // src/image-loader.ts
3574
- var MAX_IMAGE_BYTES = 6 * 1024 * 1024;
3575
- 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;
3576
- var RAW_HOSTS = /* @__PURE__ */ new Set([
3577
- "raw.githubusercontent.com",
3578
- "camo.githubusercontent.com",
3579
- "user-images.githubusercontent.com",
3580
- "private-user-images.githubusercontent.com",
3581
- "avatars.githubusercontent.com",
3582
- "objects.githubusercontent.com"
3583
- ]);
3584
- var RASTER_EXT = /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)(?:[?#]|$)/i;
3585
- var CACHE_LIMIT = 24;
3586
- var MAX_CONCURRENT_IMAGE_FETCHES = 4;
3587
- var cache = /* @__PURE__ */ new Map();
3588
- var activeFetches = 0;
3589
- var waiting = [];
3590
- function releaseSlot() {
3591
- activeFetches--;
3592
- waiting.shift()?.();
3593
- }
3594
- async function withSlot(job) {
3595
- if (activeFetches >= MAX_CONCURRENT_IMAGE_FETCHES) {
3596
- await new Promise((resolve) => waiting.push(resolve));
3597
- }
3598
- activeFetches++;
3599
- try {
3600
- return await job();
3601
- } finally {
3602
- releaseSlot();
3603
- }
3604
- }
3605
- function decodeDataUri(uri) {
3606
- const match = /^data:([^;,]+)?(;base64)?,(.*)$/is.exec(uri);
3607
- if (!match) return null;
3608
- const mime = (match[1] ?? "").toLowerCase();
3609
- if (mime.includes("svg")) return null;
3610
- const payload = match[3];
3611
- try {
3612
- if (match[2]) return new Uint8Array(Buffer.from(payload, "base64"));
3613
- return new Uint8Array(Buffer.from(decodeURIComponent(payload), "binary"));
3614
- } catch {
3615
- return null;
3616
- }
3617
- }
3618
- function toRawGitHubUrl(url) {
3619
- if (url.hostname.toLowerCase() !== "github.com") return url;
3620
- const match = /^\/([^/]+)\/([^/]+)\/(?:blob|raw)\/(.+)$/.exec(url.pathname);
3621
- if (!match) return url;
3622
- const [, owner, repo, rest] = match;
3623
- return new URL(
3624
- `https://raw.githubusercontent.com/${owner}/${repo}/${rest}${url.search}`
3625
- );
3626
- }
3627
- function resolveRootRelative(path, base) {
3628
- let parsed;
3629
- try {
3630
- parsed = new URL(base);
3631
- } catch {
3632
- return null;
3633
- }
3634
- if (parsed.hostname.toLowerCase() !== "raw.githubusercontent.com") {
3635
- return null;
3636
- }
3637
- const segments = parsed.pathname.split("/").filter(Boolean);
3638
- if (segments.length < 3) return null;
3639
- return new URL(`${parsed.origin}/${segments.slice(0, 3).join("/")}${path}`);
3640
- }
3641
- function resolveReadmeImageUrl(raw, base) {
3642
- const cleaned = raw.trim();
3643
- if (!cleaned) return null;
3644
- if (/^data:/i.test(cleaned)) {
3645
- return isRenderableImageUrl(cleaned) ? cleaned : null;
3646
- }
3647
- let url = null;
3648
- try {
3649
- url = cleaned.startsWith("/") ? resolveRootRelative(cleaned, base) : new URL(cleaned, base);
3650
- } catch {
3651
- return null;
3652
- }
3653
- if (!url) return null;
3654
- url = toRawGitHubUrl(url);
3655
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
3656
- url.hash = "";
3657
- const resolved = url.toString();
3658
- return isRenderableImageUrl(resolved) ? resolved : null;
3659
- }
3660
- function isRenderableImageUrl(url) {
3661
- if (/^data:/i.test(url)) return !/svg/i.test(url.slice(0, 64));
3662
- let parsed;
3663
- try {
3664
- parsed = new URL(url);
3665
- } catch {
3666
- return false;
3667
- }
3668
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
3669
- if (BADGE_HOST.test(parsed.hostname)) return false;
3670
- const path = parsed.pathname.toLowerCase();
3671
- if (path.endsWith(".svg") || path.endsWith(".svgz")) return false;
3672
- if (parsed.hostname.toLowerCase().endsWith("github.com")) {
3673
- if (/^\/user-attachments\//.test(path)) return true;
3674
- if (!RAW_HOSTS.has(parsed.hostname)) {
3675
- return /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)$/i.test(path);
3676
- }
3677
- }
3678
- if (/\.[a-z0-9]{1,5}$/.test(path) && !RASTER_EXT.test(path)) return false;
3679
- return true;
3680
- }
3681
- function isGitHubHost(hostname) {
3682
- return /(^|\.)githubusercontent\.com$|(^|\.)github\.com$/i.test(hostname);
3683
- }
3684
- async function fetchImageBytes(url, opts = {}) {
3685
- const cached2 = cache.get(url);
3686
- if (cached2) return cached2;
3687
- const pending = withSlot(() => loadImageBytes(url, opts));
3688
- cache.set(url, pending);
3689
- while (cache.size > CACHE_LIMIT) {
3690
- const oldest = cache.keys().next().value;
3691
- if (oldest === void 0) break;
3692
- cache.delete(oldest);
3693
- }
3694
- return pending;
3695
- }
3696
- async function loadImageBytes(url, opts) {
3697
- if (/^data:/i.test(url)) return decodeDataUri(url);
3698
- const fetchImpl = opts.fetchImpl ?? fetch;
3699
- const timeoutMs = opts.timeoutMs ?? 1e4;
3700
- const controller = new AbortController();
3701
- const timer = setTimeout(() => controller.abort(), timeoutMs);
3702
- try {
3703
- const headers = {
3704
- "User-Agent": "ghfind/1.0",
3705
- Accept: "image/*,*/*;q=0.5"
3706
- };
3707
- let host = "";
3708
- try {
3709
- host = new URL(url).hostname;
3710
- } catch {
3711
- return null;
3712
- }
3713
- if (opts.token && isGitHubHost(host)) {
3714
- headers.Authorization = `Bearer ${opts.token}`;
3715
- }
3716
- const res = await fetchImpl(url, { headers, signal: controller.signal });
3717
- if (!res.ok) return null;
3718
- const contentType = res.headers.get("content-type") ?? "";
3719
- if (contentType && !/^image\//i.test(contentType)) {
3720
- const githubHost = isGitHubHost(host);
3721
- if (!githubHost || !/octet-stream/i.test(contentType)) return null;
3722
- }
3723
- if (/svg/i.test(contentType)) return null;
3724
- const declared = Number(res.headers.get("content-length") ?? "0");
3725
- if (declared > MAX_IMAGE_BYTES) return null;
3726
- const buffer = new Uint8Array(await res.arrayBuffer());
3727
- if (buffer.byteLength === 0 || buffer.byteLength > MAX_IMAGE_BYTES) {
3728
- return null;
3729
- }
3730
- return buffer;
3731
- } catch {
3732
- return null;
3733
- } finally {
3734
- clearTimeout(timer);
3735
- }
3736
- }
3737
- function readmeBaseUrl(owner, repo, branch, path = "README.md") {
3738
- return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
3739
- }
3740
-
3741
3872
  // src/markdown-images.ts
3742
3873
  var IMG_TAG = /<img\b[^>]*?src\s*=\s*["']([^"']+)["'][^>]*>/gi;
3743
3874
  var ALT_ATTR = /\balt\s*=\s*["']([^"']*)["']/i;
@@ -4017,29 +4148,6 @@ function createMarkdownView(opts) {
4017
4148
  };
4018
4149
  }
4019
4150
 
4020
- // src/readme.ts
4021
- var CANDIDATES = [
4022
- ["main", "README.md"],
4023
- ["master", "README.md"],
4024
- ["main", "README.rst"]
4025
- ];
4026
- async function fetchReadme(owner, repo, opts = {}) {
4027
- const fetchImpl = opts.fetchImpl ?? fetch;
4028
- const headers = { "User-Agent": "ghfind/1.0" };
4029
- if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
4030
- const attempts = CANDIDATES.map(async ([branch, file]) => {
4031
- const url = readmeBaseUrl(owner, repo, branch, file);
4032
- const res = await fetchImpl(url, { headers });
4033
- if (!res.ok) throw new Error(`no readme at ${url}`);
4034
- return { text: await res.text(), sourceUrl: url };
4035
- });
4036
- try {
4037
- return await Promise.any(attempts);
4038
- } catch {
4039
- return null;
4040
- }
4041
- }
4042
-
4043
4151
  // src/explore.ts
4044
4152
  async function fetchTopics() {
4045
4153
  const res = await fetch(
@@ -4189,23 +4297,23 @@ function nextTip() {
4189
4297
  }
4190
4298
 
4191
4299
  // src/update-check.ts
4192
- import { readFileSync as readFileSync5 } from "fs";
4193
- import { join as join5, dirname as dirname3, posix } from "path";
4300
+ import { readFileSync as readFileSync6 } from "fs";
4301
+ import { join as join6, dirname as dirname3, posix } from "path";
4194
4302
  import { fileURLToPath as fileURLToPath3 } from "url";
4195
4303
  var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
4196
4304
  var PKG_NAME2 = "github-search-cli";
4197
4305
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
4198
4306
  var REQUEST_TIMEOUT_MS = 5e3;
4199
4307
  var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
4200
- var PKG_DIR2 = join5(__dirname2, "..");
4308
+ var PKG_DIR2 = join6(__dirname2, "..");
4201
4309
  var STATE_FILE = "update-state.json";
4202
- var DEFAULTS = {
4310
+ var DEFAULTS2 = {
4203
4311
  lastCheck: 0,
4204
4312
  snoozeUntil: 0,
4205
4313
  suppressed: false
4206
4314
  };
4207
4315
  function readUpdateState() {
4208
- return { ...DEFAULTS, ...readJSON(STATE_FILE, {}) };
4316
+ return { ...DEFAULTS2, ...readJSON(STATE_FILE, {}) };
4209
4317
  }
4210
4318
  function writeUpdateState(state) {
4211
4319
  writeJSON(STATE_FILE, state);
@@ -4338,14 +4446,14 @@ function recordPreUpdateState(currentVersion) {
4338
4446
  }
4339
4447
  function fetchReleaseNotes(version) {
4340
4448
  try {
4341
- const notesPath = join5(PKG_DIR2, "release-notes", `${version}.md`);
4342
- const md = readFileSync5(notesPath, "utf-8");
4449
+ const notesPath = join6(PKG_DIR2, "release-notes", `${version}.md`);
4450
+ const md = readFileSync6(notesPath, "utf-8");
4343
4451
  if (md.trim().length > 0) return md;
4344
4452
  } catch {
4345
4453
  }
4346
4454
  try {
4347
- const changelogPath = join5(PKG_DIR2, "CHANGELOG.md");
4348
- const changelog = readFileSync5(changelogPath, "utf-8");
4455
+ const changelogPath = join6(PKG_DIR2, "CHANGELOG.md");
4456
+ const changelog = readFileSync6(changelogPath, "utf-8");
4349
4457
  const header = `## [${version}]`;
4350
4458
  const start = changelog.indexOf(header);
4351
4459
  if (start === -1) return null;
@@ -5942,7 +6050,8 @@ async function launchBrowser(theme_override) {
5942
6050
  showOverlay("readme");
5943
6051
  try {
5944
6052
  const readme = await fetchReadme(repo.owner, repo.name, {
5945
- token: githubToken
6053
+ token: githubToken,
6054
+ private: repo.private
5946
6055
  });
5947
6056
  if (readme) {
5948
6057
  readmeView.setContent(readme.text, readme.sourceUrl);
@@ -6374,10 +6483,12 @@ async function launchBrowser(theme_override) {
6374
6483
  }
6375
6484
  }
6376
6485
  } catch (err) {
6377
- currentRepos = [];
6378
- resultsSelect.options = [
6379
- { name: " (error)", description: "", value: null }
6380
- ];
6486
+ if (!append) {
6487
+ currentRepos = [];
6488
+ resultsSelect.options = [
6489
+ { name: " (error)", description: "", value: null }
6490
+ ];
6491
+ }
6381
6492
  const msg = err instanceof SearchCliError ? err.userMessage : err instanceof Error ? err.message : String(err);
6382
6493
  statusMgr.set("error", msg.slice(0, 60));
6383
6494
  }
@@ -6895,16 +7006,11 @@ ${pack.description ?? ""}`;
6895
7006
  renderer.requestRender();
6896
7007
  }
6897
7008
  function formatResultLine(repo) {
6898
- const stars = formatStars(repo.stars);
7009
+ const stars = `\u2605 ${fmtStars(repo.stars)}`;
6899
7010
  const lang = (repo.language ?? "?").padEnd(12).slice(0, 12);
6900
7011
  const name = repo.fullName.padEnd(40).slice(0, 40);
6901
7012
  return `${name} ${lang} ${stars}`;
6902
7013
  }
6903
- function formatStars(n) {
6904
- if (n >= 1e6) return `\u2605 ${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
6905
- if (n >= 1e3) return `\u2605 ${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
6906
- return `\u2605 ${n}`;
6907
- }
6908
7014
  function formatToolbar(sort, limit, totalCount) {
6909
7015
  const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
6910
7016
  return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
@@ -7034,14 +7140,15 @@ function formatSkillCatalog() {
7034
7140
  }
7035
7141
 
7036
7142
  // src/skill-finder.ts
7037
- import { readdirSync, readFileSync as readFileSync6 } from "fs";
7038
- import { homedir as homedir3 } from "os";
7039
- import { delimiter, join as join6 } from "path";
7143
+ import { readdirSync, readFileSync as readFileSync7 } from "fs";
7144
+ import { homedir as homedir4 } from "os";
7145
+ import { delimiter, join as join7 } from "path";
7040
7146
  var REGISTRY_SEARCH_ENDPOINT = "https://skills.sh/api/search";
7041
7147
  var MIN_QUERY_LENGTH = 2;
7042
7148
  var MAX_SKILL_RESULTS = 100;
7149
+ var SAFE_ID_RE = /^[A-Za-z0-9._/-]+$/;
7043
7150
  function displayPath(path) {
7044
- const home = homedir3();
7151
+ const home = homedir4();
7045
7152
  const normalized = path.replace(/\\/g, "/");
7046
7153
  const homeNorm = home.replace(/\\/g, "/").replace(/\/$/, "");
7047
7154
  if (normalized === homeNorm) return "~";
@@ -7051,12 +7158,12 @@ function displayPath(path) {
7051
7158
  return normalized;
7052
7159
  }
7053
7160
  function defaultSkillRoots(cwd = process.cwd()) {
7054
- const home = homedir3();
7161
+ const home = homedir4();
7055
7162
  return [
7056
- join6(home, ".codex", "skills"),
7057
- join6(home, ".agents", "skills"),
7058
- join6(cwd, ".codex", "skills"),
7059
- join6(cwd, ".github", "skills")
7163
+ join7(home, ".codex", "skills"),
7164
+ join7(home, ".agents", "skills"),
7165
+ join7(cwd, ".codex", "skills"),
7166
+ join7(cwd, ".github", "skills")
7060
7167
  ];
7061
7168
  }
7062
7169
  function resolveSkillRoots(cwd = process.cwd()) {
@@ -7098,16 +7205,20 @@ function parseSkillFrontmatter(text) {
7098
7205
  if (!name) return null;
7099
7206
  return { name, description: fields.description ?? "" };
7100
7207
  }
7208
+ var scanCache = /* @__PURE__ */ new Map();
7101
7209
  function scanLocalSkills(roots = resolveSkillRoots()) {
7210
+ const key = roots.map((r) => `${r.path}\0${r.label}`).join("");
7211
+ const cached2 = scanCache.get(key);
7212
+ if (cached2) return cached2;
7102
7213
  const byName = /* @__PURE__ */ new Map();
7103
7214
  for (const root of roots) {
7104
7215
  for (const dir of walkDirs(root.path)) {
7105
7216
  const entries = listDir(dir);
7106
7217
  if (!entries) continue;
7107
7218
  if (!entries.some((e) => e.isFile() && e.name === "SKILL.md")) continue;
7108
- const file = join6(dir, "SKILL.md");
7219
+ const file = join7(dir, "SKILL.md");
7109
7220
  try {
7110
- const parsed = parseSkillFrontmatter(readFileSync6(file, "utf-8"));
7221
+ const parsed = parseSkillFrontmatter(readFileSync7(file, "utf-8"));
7111
7222
  if (!parsed || byName.has(parsed.name)) continue;
7112
7223
  byName.set(parsed.name, {
7113
7224
  name: parsed.name,
@@ -7119,7 +7230,9 @@ function scanLocalSkills(roots = resolveSkillRoots()) {
7119
7230
  }
7120
7231
  }
7121
7232
  }
7122
- return [...byName.values()];
7233
+ const hits = [...byName.values()];
7234
+ scanCache.set(key, hits);
7235
+ return hits;
7123
7236
  }
7124
7237
  function listDir(dir) {
7125
7238
  try {
@@ -7138,7 +7251,7 @@ function* walkDirs(root) {
7138
7251
  if (depth >= MAX_DEPTH) continue;
7139
7252
  for (const entry of entries) {
7140
7253
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
7141
- queue.push([join6(dir, entry.name), depth + 1]);
7254
+ queue.push([join7(dir, entry.name), depth + 1]);
7142
7255
  }
7143
7256
  }
7144
7257
  }
@@ -7184,7 +7297,10 @@ function normalizeRegistrySkill(raw) {
7184
7297
  const obj = raw;
7185
7298
  const source = typeof obj.source === "string" ? obj.source : "";
7186
7299
  const skillId = typeof obj.skillId === "string" ? obj.skillId : "";
7187
- if (!source || !skillId) return null;
7300
+ const hasTraversal = (s) => s === ".." || s.includes("..");
7301
+ if (!SAFE_ID_RE.test(source) || !SAFE_ID_RE.test(skillId) || hasTraversal(source) || hasTraversal(skillId)) {
7302
+ return null;
7303
+ }
7188
7304
  const name = typeof obj.name === "string" && obj.name.trim() ? obj.name : skillId;
7189
7305
  const installs = typeof obj.installs === "number" ? obj.installs : 0;
7190
7306
  const id = typeof obj.id === "string" && obj.id ? obj.id : `${source}/${skillId}`;
@@ -7580,19 +7696,14 @@ var TOOLS = [
7580
7696
  throw new McpToolError("repos must contain 2-5 repo fullNames.");
7581
7697
  }
7582
7698
  const token = resolveToken(state, args.token);
7583
- const provider = createGitHubSearch(void 0, token ? [token] : []);
7584
7699
  const found = [];
7585
7700
  const missing = [];
7586
7701
  for (const fullName of names) {
7587
- const res = await provider.search(parseQuery(fullName), {
7588
- limit: 1,
7589
- sort: "stars",
7590
- json: false,
7591
- verbose: false,
7592
- token
7593
- });
7594
- if (res.repos.length > 0) found.push(res.repos[0]);
7595
- else missing.push(fullName);
7702
+ try {
7703
+ found.push(await resolveRepoFromRef(fullName, token));
7704
+ } catch {
7705
+ missing.push(fullName);
7706
+ }
7596
7707
  }
7597
7708
  if (found.length < 2) {
7598
7709
  throw new McpToolError(
@@ -7895,7 +8006,16 @@ async function runMcpServer(options = {}) {
7895
8006
  // src/cli.ts
7896
8007
  process.env.GHFIND_CLI_RUN = "1";
7897
8008
  var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
7898
- var PKG_DIR3 = join7(__dirname3, "..");
8009
+ var PKG_DIR3 = join8(__dirname3, "..");
8010
+ var SORT_VALUES = [
8011
+ "best-match",
8012
+ "stars",
8013
+ "updated",
8014
+ "forks",
8015
+ "score",
8016
+ "downloads",
8017
+ "name"
8018
+ ];
7899
8019
  function parseArgs(args) {
7900
8020
  const flags = {
7901
8021
  query: "",
@@ -7994,9 +8114,17 @@ function parseArgs(args) {
7994
8114
  }
7995
8115
  break;
7996
8116
  }
7997
- case "--sort":
7998
- flags.sort = args[++i];
8117
+ case "--sort": {
8118
+ const sort = args[++i];
8119
+ if (!sort || !SORT_VALUES.includes(sort)) {
8120
+ console.error(
8121
+ `Invalid sort: ${sort ?? "(missing)"} (use best-match|stars|updated|forks).`
8122
+ );
8123
+ process.exit(1);
8124
+ }
8125
+ flags.sort = sort;
7999
8126
  break;
8127
+ }
8000
8128
  case "--token":
8001
8129
  flags.token = args[++i];
8002
8130
  break;
@@ -8209,8 +8337,8 @@ Options:
8209
8337
  if (flags.completion) {
8210
8338
  const shell = flags.completion;
8211
8339
  try {
8212
- const content = readFileSync7(
8213
- join7(PKG_DIR3, "completions", `ghfind.${shell}`),
8340
+ const content = readFileSync8(
8341
+ join8(PKG_DIR3, "completions", `ghfind.${shell}`),
8214
8342
  "utf-8"
8215
8343
  );
8216
8344
  console.log(content);
@@ -8524,10 +8652,11 @@ function rowsToCsv(header, rows) {
8524
8652
  return [header, ...rows].map((row) => row.map(quote).join(",")).join("\n");
8525
8653
  }
8526
8654
  function rowsToMarkdown(header, rows) {
8655
+ const cell = (value) => value.replace(/\|/g, "\\|");
8527
8656
  return [
8528
- `| ${header.join(" | ")} |`,
8657
+ `| ${header.map(cell).join(" | ")} |`,
8529
8658
  `|${header.map(() => "---").join("|")}|`,
8530
- ...rows.map((row) => `| ${row.join(" | ")} |`)
8659
+ ...rows.map((row) => `| ${row.map(cell).join(" | ")} |`)
8531
8660
  ].join("\n");
8532
8661
  }
8533
8662
  function truncate2(text, width) {
@@ -8567,7 +8696,12 @@ async function runReadme(flags) {
8567
8696
  }
8568
8697
  try {
8569
8698
  const { owner, name } = repoFromRef(ref);
8570
- const readme = await fetchReadme(owner, name, { token: flags.token });
8699
+ const readme = await fetchReadme(owner, name, {
8700
+ // A bare ref carries no visibility info, so the token is never
8701
+ // forwarded to raw.githubusercontent.com.
8702
+ token: flags.token,
8703
+ private: false
8704
+ });
8571
8705
  if (!readme) {
8572
8706
  console.error(`No README found for ${owner}/${name}.`);
8573
8707
  process.exit(1);
@@ -8816,22 +8950,14 @@ async function runNonInteractive(flags, outputFormat) {
8816
8950
  }
8817
8951
  },
8818
8952
  compare: async (_ctx) => {
8819
- const search = createGitHubSearch(void 0, [flags.token ?? ""]);
8820
8953
  const repos = [];
8821
8954
  const missing = [];
8822
8955
  for (const fullName of flags.compare) {
8823
- const res = await search.search(
8824
- { keywords: [fullName], qualifiers: [], raw: fullName },
8825
- {
8826
- limit: 1,
8827
- sort: "stars",
8828
- json: false,
8829
- verbose: false,
8830
- token: flags.token
8831
- }
8832
- );
8833
- if (res.repos.length > 0) repos.push(...res.repos);
8834
- else missing.push(fullName);
8956
+ try {
8957
+ repos.push(await resolveRepoFromRef(fullName, flags.token));
8958
+ } catch {
8959
+ missing.push(fullName);
8960
+ }
8835
8961
  }
8836
8962
  if (repos.length < 2) {
8837
8963
  console.error(
@@ -8858,10 +8984,8 @@ Not found: ${missing.join(", ")}` : "";
8858
8984
  await handlers[key](ctx);
8859
8985
  }
8860
8986
  main().catch((err) => {
8861
- if (err instanceof Error && err.name === "SearchCliError") {
8862
- console.error(
8863
- err instanceof SearchCliError ? err.userMessage : err.message
8864
- );
8987
+ if (err instanceof SearchCliError) {
8988
+ console.error(err.userMessage);
8865
8989
  } else {
8866
8990
  console.error(err instanceof Error ? err.message : String(err));
8867
8991
  reportError(err);