github-search-cli 9.8.1 → 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/CHANGELOG.md +120 -0
- package/LICENSE +21 -21
- package/README.md +45 -25
- package/completions/ghfind.bash +55 -55
- package/completions/ghfind.fish +28 -28
- package/completions/ghfind.zsh +52 -52
- package/demo.svg +71 -71
- package/dist/cli.js +1097 -735
- package/package.json +1 -1
- package/release-notes/9.8.2.md +65 -0
- package/release-notes/9.8.3.md +98 -0
- package/scripts/brew.rb +53 -53
- package/scripts/postinstall.js +38 -0
- package/src/cli.ts +40 -22
- package/src/config.ts +12 -2
- package/src/deepdive.ts +70 -66
- package/src/doctor.ts +3 -2
- package/src/error-report.ts +81 -11
- package/src/errors.ts +27 -1
- package/src/help.ts +11 -0
- package/src/history.ts +2 -5
- package/src/image-loader.ts +57 -2
- package/src/init.ts +1 -2
- package/src/markdown-images.ts +93 -4
- package/src/markdown-view.ts +49 -20
- package/src/mcp-server.ts +22 -11
- package/src/notifications.ts +9 -3
- package/src/org.ts +15 -14
- package/src/output.ts +1 -13
- package/src/readme.ts +19 -3
- package/src/releases.ts +29 -27
- package/src/search.ts +23 -2
- package/src/session.ts +58 -2
- package/src/skill-finder.ts +33 -2
- package/src/storage.ts +22 -47
- package/src/tui.ts +190 -44
- package/src/user.ts +15 -14
- package/dist/chunk-GYIL4S5X.js +0 -59
- package/dist/config-OCIPF2IF.js +0 -14
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
|
|
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() {
|
|
@@ -26,6 +16,20 @@ var NetworkError = class extends SearchCliError {
|
|
|
26
16
|
this.name = "NetworkError";
|
|
27
17
|
}
|
|
28
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
|
+
};
|
|
29
33
|
var RateLimitError = class extends SearchCliError {
|
|
30
34
|
constructor(hasToken, resetSeconds) {
|
|
31
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";
|
|
@@ -154,6 +158,7 @@ var MemoryCache = class {
|
|
|
154
158
|
return entry.data;
|
|
155
159
|
}
|
|
156
160
|
set(key, data, ttlMs) {
|
|
161
|
+
this.cache.delete(key);
|
|
157
162
|
if (this.cache.size >= MAX_CACHE_SIZE) {
|
|
158
163
|
const firstKey = this.cache.keys().next().value;
|
|
159
164
|
if (firstKey !== void 0) this.cache.delete(firstKey);
|
|
@@ -439,7 +444,14 @@ var GitHubSearchAdapter = class {
|
|
|
439
444
|
this.logger.error(
|
|
440
445
|
`[github] non-rate-limit 403: ${body.slice(0, 200)}`
|
|
441
446
|
);
|
|
442
|
-
throw new
|
|
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();
|
|
443
455
|
}
|
|
444
456
|
if (!res.ok) {
|
|
445
457
|
const body = await res.text().catch(() => "");
|
|
@@ -476,6 +488,7 @@ var GitHubSearchAdapter = class {
|
|
|
476
488
|
lastErr = void 0;
|
|
477
489
|
break;
|
|
478
490
|
} catch (err) {
|
|
491
|
+
if (err instanceof RateLimitError) throw err;
|
|
479
492
|
lastErr = err;
|
|
480
493
|
if (rateLimited) break;
|
|
481
494
|
}
|
|
@@ -1190,13 +1203,199 @@ async function launchPackageBrowser() {
|
|
|
1190
1203
|
}
|
|
1191
1204
|
|
|
1192
1205
|
// src/doctor.ts
|
|
1193
|
-
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
1206
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
1194
1207
|
import { spawnSync } from "child_process";
|
|
1195
|
-
import { join, dirname } from "path";
|
|
1208
|
+
import { join as join3, dirname } from "path";
|
|
1196
1209
|
import { fileURLToPath } from "url";
|
|
1197
|
-
import { homedir, platform, arch } from "os";
|
|
1210
|
+
import { homedir as homedir3, platform, arch } from "os";
|
|
1211
|
+
|
|
1212
|
+
// src/config.ts
|
|
1213
|
+
import { homedir } from "os";
|
|
1214
|
+
import { join } from "path";
|
|
1215
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1216
|
+
function stateDir() {
|
|
1217
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
1218
|
+
const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".local", "share", "ghfind");
|
|
1219
|
+
return base;
|
|
1220
|
+
}
|
|
1221
|
+
function cacheDir() {
|
|
1222
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
1223
|
+
const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".cache", "ghfind");
|
|
1224
|
+
return base;
|
|
1225
|
+
}
|
|
1226
|
+
var DEFAULTS = {
|
|
1227
|
+
defaultSort: "best-match",
|
|
1228
|
+
defaultLimit: 50,
|
|
1229
|
+
theme: "tokyo-night",
|
|
1230
|
+
cacheTtlSeconds: 300,
|
|
1231
|
+
defaultTab: "search"
|
|
1232
|
+
};
|
|
1233
|
+
function configPath() {
|
|
1234
|
+
const envPath = process.env.GHFIND_CONFIG;
|
|
1235
|
+
if (envPath) return envPath;
|
|
1236
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
1237
|
+
const base = xdg ? join(xdg, "ghfind") : join(homedir(), ".config", "ghfind");
|
|
1238
|
+
return join(base, "config.json");
|
|
1239
|
+
}
|
|
1240
|
+
function loadConfig() {
|
|
1241
|
+
const path = configPath();
|
|
1242
|
+
try {
|
|
1243
|
+
const raw = readFileSync(path, "utf-8");
|
|
1244
|
+
const parsed = JSON.parse(raw);
|
|
1245
|
+
return mergeWithEnv({ ...DEFAULTS, ...parsed });
|
|
1246
|
+
} catch {
|
|
1247
|
+
return mergeWithEnv({ ...DEFAULTS });
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function saveConfig(config) {
|
|
1251
|
+
const path = configPath();
|
|
1252
|
+
const dir = path.slice(
|
|
1253
|
+
0,
|
|
1254
|
+
Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
|
|
1255
|
+
);
|
|
1256
|
+
mkdirSync(dir, { recursive: true });
|
|
1257
|
+
writeFileSync(path, JSON.stringify(config, null, 2), {
|
|
1258
|
+
encoding: "utf-8",
|
|
1259
|
+
mode: 384
|
|
1260
|
+
});
|
|
1261
|
+
try {
|
|
1262
|
+
chmodSync(path, 384);
|
|
1263
|
+
} catch {
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function mergeWithEnv(config) {
|
|
1267
|
+
if (process.env.GITHUB_TOKEN) config.githubToken = process.env.GITHUB_TOKEN;
|
|
1268
|
+
return config;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/login.ts
|
|
1272
|
+
import { createInterface } from "readline";
|
|
1273
|
+
import { homedir as homedir2 } from "os";
|
|
1274
|
+
import { join as join2 } from "path";
|
|
1275
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
1276
|
+
var TOKEN_PREFIXES = ["ghp_", "gho_", "github_pat_"];
|
|
1277
|
+
function isLikelyGithubToken(token) {
|
|
1278
|
+
return TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix));
|
|
1279
|
+
}
|
|
1280
|
+
function maskToken(token) {
|
|
1281
|
+
if (token.length <= 11) return `${token.slice(0, 4)}\u2026`;
|
|
1282
|
+
return `${token.slice(0, 7)}\u2026${token.slice(-4)}`;
|
|
1283
|
+
}
|
|
1284
|
+
function ghCliConfigDir() {
|
|
1285
|
+
return process.env.GH_CONFIG_DIR || join2(homedir2(), ".config", "gh");
|
|
1286
|
+
}
|
|
1287
|
+
function parseHostsYmlToken(text, host = "github.com") {
|
|
1288
|
+
let inHost = false;
|
|
1289
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1290
|
+
const trimmed = line.trim();
|
|
1291
|
+
if (/^[A-Za-z0-9.-]+:\s*$/.test(trimmed)) {
|
|
1292
|
+
inHost = trimmed.slice(0, -1).trim() === host;
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
if (inHost) {
|
|
1296
|
+
const match = trimmed.match(/^oauth_token:\s*["']?([^"'\s]+)/);
|
|
1297
|
+
if (match) return match[1];
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return void 0;
|
|
1301
|
+
}
|
|
1302
|
+
function readGhCliToken(ghDir = ghCliConfigDir(), host = process.env.GH_HOST ?? "github.com") {
|
|
1303
|
+
const hostsFile = join2(ghDir, "hosts.yml");
|
|
1304
|
+
if (!existsSync(hostsFile)) return void 0;
|
|
1305
|
+
try {
|
|
1306
|
+
return parseHostsYmlToken(readFileSync2(hostsFile, "utf-8"), host);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return void 0;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function persistToken(token, deps) {
|
|
1312
|
+
const config = deps.readConfig();
|
|
1313
|
+
config.githubToken = token;
|
|
1314
|
+
deps.writeConfig(config);
|
|
1315
|
+
}
|
|
1316
|
+
function createAsk() {
|
|
1317
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1318
|
+
return (query) => new Promise((resolve) => {
|
|
1319
|
+
rl.question(query, (answer) => {
|
|
1320
|
+
rl.close();
|
|
1321
|
+
resolve(answer.trim());
|
|
1322
|
+
});
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
function defaultLoginDeps() {
|
|
1326
|
+
return {
|
|
1327
|
+
ask: createAsk(),
|
|
1328
|
+
readGhCliToken: () => readGhCliToken(),
|
|
1329
|
+
readConfig: loadConfig,
|
|
1330
|
+
writeConfig: (config) => saveConfig(config)
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function successNote() {
|
|
1334
|
+
return [
|
|
1335
|
+
" Authentication raises the search rate limit from 60 to",
|
|
1336
|
+
" 5,000 requests/hour. Remove the token anytime by editing",
|
|
1337
|
+
` ${configPath()} \u2014 delete the "githubToken" line and save.`
|
|
1338
|
+
];
|
|
1339
|
+
}
|
|
1340
|
+
async function runLoginWizard(deps = defaultLoginDeps()) {
|
|
1341
|
+
const { ask: ask2 } = deps;
|
|
1342
|
+
console.log("");
|
|
1343
|
+
console.log(" ghfind login");
|
|
1344
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1345
|
+
console.log("");
|
|
1346
|
+
if (process.env.GITHUB_TOKEN) {
|
|
1347
|
+
console.log(
|
|
1348
|
+
" \u2139 GITHUB_TOKEN is set in the environment and already takes precedence."
|
|
1349
|
+
);
|
|
1350
|
+
console.log("");
|
|
1351
|
+
}
|
|
1352
|
+
const current = deps.readConfig();
|
|
1353
|
+
if (current.githubToken) {
|
|
1354
|
+
console.log(
|
|
1355
|
+
` \u2713 A token is already saved: ${maskToken(current.githubToken)}`
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
const ghToken = deps.readGhCliToken();
|
|
1359
|
+
if (ghToken) {
|
|
1360
|
+
console.log(" \u2713 Found a token managed by the GitHub CLI (gh).");
|
|
1361
|
+
const answer = await ask2(" Import it into ghfind? [Y/n]: ");
|
|
1362
|
+
if (answer.toLowerCase() !== "n") {
|
|
1363
|
+
persistToken(ghToken, deps);
|
|
1364
|
+
finishLogin(ghToken);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const pasted = await ask2(" Paste a GitHub token (Enter to skip): ");
|
|
1369
|
+
if (!pasted) {
|
|
1370
|
+
console.log(" Skipped \u2014 no token saved.");
|
|
1371
|
+
console.log("");
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (!isLikelyGithubToken(pasted)) {
|
|
1375
|
+
console.log(
|
|
1376
|
+
` \u26A0 That doesn't look like a GitHub token (expected one of: ${TOKEN_PREFIXES.join(", ")}).`
|
|
1377
|
+
);
|
|
1378
|
+
const confirm = await ask2(" Save it anyway? [y/N]: ");
|
|
1379
|
+
if (confirm.toLowerCase() !== "y") {
|
|
1380
|
+
console.log(" Cancelled \u2014 nothing saved.");
|
|
1381
|
+
console.log("");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
persistToken(pasted, deps);
|
|
1386
|
+
finishLogin(pasted);
|
|
1387
|
+
}
|
|
1388
|
+
function finishLogin(token) {
|
|
1389
|
+
console.log(` \u2713 Token saved to ${configPath()}`);
|
|
1390
|
+
console.log(` (masked: ${maskToken(token)})`);
|
|
1391
|
+
console.log("");
|
|
1392
|
+
for (const line of successNote()) console.log(line);
|
|
1393
|
+
console.log("");
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/doctor.ts
|
|
1198
1397
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1199
|
-
var PKG_DIR =
|
|
1398
|
+
var PKG_DIR = join3(__dirname, "..");
|
|
1200
1399
|
var results = [];
|
|
1201
1400
|
function add(status, label, detail) {
|
|
1202
1401
|
results.push({ status, label, detail });
|
|
@@ -1222,7 +1421,7 @@ function hasSystemBun() {
|
|
|
1222
1421
|
return res.status === 0 ? { ok: true, version: res.stdout.trim() } : { ok: false };
|
|
1223
1422
|
}
|
|
1224
1423
|
function bundledBunPath() {
|
|
1225
|
-
return
|
|
1424
|
+
return join3(PKG_DIR, "vendor", platform() === "win32" ? "bun.exe" : "bun");
|
|
1226
1425
|
}
|
|
1227
1426
|
function bunRuns(bin) {
|
|
1228
1427
|
const res = run(bin, ["--version"]);
|
|
@@ -1232,7 +1431,7 @@ function bunRuns(bin) {
|
|
|
1232
1431
|
function detectLibc() {
|
|
1233
1432
|
if (platform() !== "linux") return "unknown";
|
|
1234
1433
|
try {
|
|
1235
|
-
const osRelease =
|
|
1434
|
+
const osRelease = readFileSync3("/etc/os-release", "utf8");
|
|
1236
1435
|
if (/alpine|musl/i.test(osRelease)) return "musl";
|
|
1237
1436
|
} catch {
|
|
1238
1437
|
}
|
|
@@ -1253,7 +1452,7 @@ async function checkRuntime() {
|
|
|
1253
1452
|
}
|
|
1254
1453
|
function checkBundledBun() {
|
|
1255
1454
|
const path = bundledBunPath();
|
|
1256
|
-
if (!
|
|
1455
|
+
if (!existsSync2(path)) {
|
|
1257
1456
|
add(
|
|
1258
1457
|
"WARN",
|
|
1259
1458
|
"Bundled Bun",
|
|
@@ -1302,8 +1501,8 @@ function checkNodeVersion() {
|
|
|
1302
1501
|
}
|
|
1303
1502
|
}
|
|
1304
1503
|
function checkBuildOutput() {
|
|
1305
|
-
const dist =
|
|
1306
|
-
if (
|
|
1504
|
+
const dist = join3(PKG_DIR, "dist", "cli.js");
|
|
1505
|
+
if (existsSync2(dist)) {
|
|
1307
1506
|
add("PASS", "Build output", `${dist}`);
|
|
1308
1507
|
} else {
|
|
1309
1508
|
add(
|
|
@@ -1314,8 +1513,8 @@ function checkBuildOutput() {
|
|
|
1314
1513
|
}
|
|
1315
1514
|
}
|
|
1316
1515
|
function checkSource() {
|
|
1317
|
-
const src =
|
|
1318
|
-
if (
|
|
1516
|
+
const src = join3(PKG_DIR, "src", "cli.ts");
|
|
1517
|
+
if (existsSync2(src)) {
|
|
1319
1518
|
add("PASS", "Source", `${src}`);
|
|
1320
1519
|
} else {
|
|
1321
1520
|
add("FAIL", "Source", "src/cli.ts not found. Reinstall the package.");
|
|
@@ -1331,7 +1530,7 @@ function checkTuiBackend() {
|
|
|
1331
1530
|
}
|
|
1332
1531
|
function checkConfig() {
|
|
1333
1532
|
const path = configPath();
|
|
1334
|
-
if (!
|
|
1533
|
+
if (!existsSync2(path)) {
|
|
1335
1534
|
add(
|
|
1336
1535
|
"WARN",
|
|
1337
1536
|
"Config",
|
|
@@ -1340,7 +1539,7 @@ function checkConfig() {
|
|
|
1340
1539
|
return;
|
|
1341
1540
|
}
|
|
1342
1541
|
try {
|
|
1343
|
-
const raw =
|
|
1542
|
+
const raw = readFileSync3(path, "utf-8");
|
|
1344
1543
|
JSON.parse(raw);
|
|
1345
1544
|
add("PASS", "Config", `${path} (valid JSON)`);
|
|
1346
1545
|
} catch (err) {
|
|
@@ -1355,8 +1554,8 @@ function checkGithubToken() {
|
|
|
1355
1554
|
const config = loadConfig();
|
|
1356
1555
|
const token = config.githubToken || process.env.GITHUB_TOKEN;
|
|
1357
1556
|
if (token) {
|
|
1358
|
-
const preview = token
|
|
1359
|
-
add("PASS", "GitHub token", `Configured (${preview}
|
|
1557
|
+
const preview = maskToken(token);
|
|
1558
|
+
add("PASS", "GitHub token", `Configured (${preview}).`);
|
|
1360
1559
|
} else {
|
|
1361
1560
|
add(
|
|
1362
1561
|
"WARN",
|
|
@@ -1371,7 +1570,7 @@ function checkDirs() {
|
|
|
1371
1570
|
["Cache dir", cacheDir()]
|
|
1372
1571
|
]) {
|
|
1373
1572
|
try {
|
|
1374
|
-
|
|
1573
|
+
mkdirSync2(dir, { recursive: true });
|
|
1375
1574
|
add("PASS", label, dir);
|
|
1376
1575
|
} catch (err) {
|
|
1377
1576
|
add(
|
|
@@ -1387,7 +1586,7 @@ var BIN_NAME = "ghfind";
|
|
|
1387
1586
|
function runningVersion() {
|
|
1388
1587
|
try {
|
|
1389
1588
|
const pkg = JSON.parse(
|
|
1390
|
-
|
|
1589
|
+
readFileSync3(join3(PKG_DIR, "package.json"), "utf8")
|
|
1391
1590
|
);
|
|
1392
1591
|
return pkg.version ?? "";
|
|
1393
1592
|
} catch {
|
|
@@ -1397,11 +1596,11 @@ function runningVersion() {
|
|
|
1397
1596
|
function installVersionAt(shimPath) {
|
|
1398
1597
|
const dir = dirname(shimPath);
|
|
1399
1598
|
for (const ancestor of [dir, dirname(dir)]) {
|
|
1400
|
-
for (const sub of ["node_modules",
|
|
1401
|
-
const pkgJson =
|
|
1402
|
-
if (
|
|
1599
|
+
for (const sub of ["node_modules", join3("lib", "node_modules")]) {
|
|
1600
|
+
const pkgJson = join3(ancestor, sub, PKG_NAME, "package.json");
|
|
1601
|
+
if (existsSync2(pkgJson)) {
|
|
1403
1602
|
try {
|
|
1404
|
-
const pkg = JSON.parse(
|
|
1603
|
+
const pkg = JSON.parse(readFileSync3(pkgJson, "utf8"));
|
|
1405
1604
|
return { version: pkg.version ?? "", dir: dirname(pkgJson) };
|
|
1406
1605
|
} catch {
|
|
1407
1606
|
}
|
|
@@ -1481,7 +1680,7 @@ function printReport() {
|
|
|
1481
1680
|
return `${code}${s}\x1B[0m`;
|
|
1482
1681
|
};
|
|
1483
1682
|
console.log("\n ghfind doctor \u2014 environment diagnostics");
|
|
1484
|
-
console.log(` ${platform()} ${arch} \xB7 ${
|
|
1683
|
+
console.log(` ${platform()} ${arch} \xB7 ${homedir3()}`);
|
|
1485
1684
|
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
1685
|
for (const r of results) {
|
|
1487
1686
|
console.log(
|
|
@@ -1531,20 +1730,20 @@ import {
|
|
|
1531
1730
|
import { spawn as spawn2 } from "child_process";
|
|
1532
1731
|
|
|
1533
1732
|
// src/version.ts
|
|
1534
|
-
import { readFileSync as
|
|
1535
|
-
import { join as
|
|
1733
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1734
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
1536
1735
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1537
1736
|
var cached = null;
|
|
1538
1737
|
function getVersion() {
|
|
1539
1738
|
if (cached) return cached;
|
|
1540
1739
|
if (true) {
|
|
1541
|
-
cached = "9.8.
|
|
1740
|
+
cached = "9.8.3";
|
|
1542
1741
|
return cached;
|
|
1543
1742
|
}
|
|
1544
1743
|
try {
|
|
1545
1744
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
1546
1745
|
const pkg = JSON.parse(
|
|
1547
|
-
|
|
1746
|
+
readFileSync4(join4(here, "..", "package.json"), "utf-8")
|
|
1548
1747
|
);
|
|
1549
1748
|
cached = pkg.version ?? "unknown";
|
|
1550
1749
|
} catch {
|
|
@@ -1556,6 +1755,45 @@ function getVersion() {
|
|
|
1556
1755
|
// src/error-report.ts
|
|
1557
1756
|
var REPO = "codersdfs/search-cli";
|
|
1558
1757
|
var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
|
|
1758
|
+
var SECRET_PLACEHOLDER = "***";
|
|
1759
|
+
var SECRET_FLAGS = /^(?:--token|-t)$/;
|
|
1760
|
+
var SECRET_PATTERNS = [
|
|
1761
|
+
/gh[pousr]_[A-Za-z0-9]{16,}/g,
|
|
1762
|
+
/github_pat_[A-Za-z0-9_]{20,}/g
|
|
1763
|
+
];
|
|
1764
|
+
function redactSecrets(text) {
|
|
1765
|
+
let out = text;
|
|
1766
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
1767
|
+
out = out.replace(pattern, SECRET_PLACEHOLDER);
|
|
1768
|
+
}
|
|
1769
|
+
return out;
|
|
1770
|
+
}
|
|
1771
|
+
function redactArgv(argv) {
|
|
1772
|
+
const out = [];
|
|
1773
|
+
let redactNext = false;
|
|
1774
|
+
for (const arg of argv) {
|
|
1775
|
+
if (redactNext) {
|
|
1776
|
+
out.push(SECRET_PLACEHOLDER);
|
|
1777
|
+
redactNext = false;
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
if (SECRET_FLAGS.test(arg)) {
|
|
1781
|
+
out.push(arg);
|
|
1782
|
+
redactNext = true;
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
const eq = arg.indexOf("=");
|
|
1786
|
+
if (eq > 0 && SECRET_FLAGS.test(arg.slice(0, eq))) {
|
|
1787
|
+
out.push(`${arg.slice(0, eq)}=${SECRET_PLACEHOLDER}`);
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
out.push(redactSecrets(arg));
|
|
1791
|
+
}
|
|
1792
|
+
return out;
|
|
1793
|
+
}
|
|
1794
|
+
function argvHasSecret(argv) {
|
|
1795
|
+
return redactArgv(argv).some((arg, i) => arg !== argv[i]);
|
|
1796
|
+
}
|
|
1559
1797
|
function getPackageVersion() {
|
|
1560
1798
|
return getVersion();
|
|
1561
1799
|
}
|
|
@@ -1565,9 +1803,12 @@ function runtimeName() {
|
|
|
1565
1803
|
}
|
|
1566
1804
|
function formatErrorBody(err, context) {
|
|
1567
1805
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
1568
|
-
const stack = (
|
|
1569
|
-
|
|
1570
|
-
|
|
1806
|
+
const stack = redactSecrets(
|
|
1807
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: strips ANSI color escapes from stack traces
|
|
1808
|
+
(e.stack ?? "(no stack)").replace(/\u001b\[[0-9;]*m/g, "")
|
|
1809
|
+
);
|
|
1810
|
+
const message = redactSecrets(e.message || "(no message)").slice(0, 500);
|
|
1811
|
+
const cmd = `${context.command} ${redactArgv(context.argv).join(" ")}`.trim();
|
|
1571
1812
|
const ghfindVersion = getPackageVersion();
|
|
1572
1813
|
return [
|
|
1573
1814
|
"## What happened",
|
|
@@ -1601,7 +1842,7 @@ function formatErrorBody(err, context) {
|
|
|
1601
1842
|
}
|
|
1602
1843
|
function buildIssueUrl(err, context) {
|
|
1603
1844
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
1604
|
-
const title = e.message.slice(0, 80).replace(/[\r\n]+/g, " ");
|
|
1845
|
+
const title = redactSecrets(e.message).slice(0, 80).replace(/[\r\n]+/g, " ");
|
|
1605
1846
|
const body = formatErrorBody(err, context);
|
|
1606
1847
|
const params = new URLSearchParams({
|
|
1607
1848
|
title: `[bug] ${title || "Unhandled error"}`,
|
|
@@ -1633,15 +1874,25 @@ function reportError(err, context = {
|
|
|
1633
1874
|
command: "ghfind",
|
|
1634
1875
|
argv: process.argv.slice(2)
|
|
1635
1876
|
}) {
|
|
1636
|
-
const
|
|
1877
|
+
const hadSecret = argvHasSecret(context.argv);
|
|
1878
|
+
const url = buildIssueUrl(err, {
|
|
1879
|
+
command: context.command,
|
|
1880
|
+
argv: redactArgv(context.argv)
|
|
1881
|
+
});
|
|
1637
1882
|
console.error("");
|
|
1638
1883
|
console.error(" \u26A0 ghfind encountered an unexpected error.");
|
|
1639
1884
|
console.error("");
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1885
|
+
if (hadSecret) {
|
|
1886
|
+
console.error(" A token was found in the command line and has been");
|
|
1887
|
+
console.error(" redacted. Your browser will NOT be opened \u2014 review the");
|
|
1888
|
+
console.error(" scrubbed URL below before sharing it anywhere.");
|
|
1889
|
+
} else {
|
|
1890
|
+
console.error(" Help us fix it by opening a bug report with the");
|
|
1891
|
+
console.error(" details pre-filled. Your browser will open the new-issue");
|
|
1892
|
+
console.error(" page; review and submit (or close the tab to skip).");
|
|
1893
|
+
}
|
|
1643
1894
|
console.error("");
|
|
1644
|
-
const opened = tryOpenUrl(url);
|
|
1895
|
+
const opened = hadSecret ? false : tryOpenUrl(url);
|
|
1645
1896
|
if (opened) {
|
|
1646
1897
|
console.error(` \u2192 Opened: ${url}`);
|
|
1647
1898
|
} else {
|
|
@@ -1654,7 +1905,7 @@ function reportError(err, context = {
|
|
|
1654
1905
|
}
|
|
1655
1906
|
|
|
1656
1907
|
// src/output.ts
|
|
1657
|
-
import { writeFileSync } from "fs";
|
|
1908
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1658
1909
|
var FORMATS = /* @__PURE__ */ new Map([
|
|
1659
1910
|
// Export formats
|
|
1660
1911
|
["json", formatJson],
|
|
@@ -1722,7 +1973,7 @@ function exportToFile(repos, fmt) {
|
|
|
1722
1973
|
const ext = fmt === "markdown" ? "md" : fmt;
|
|
1723
1974
|
const ts = Date.now();
|
|
1724
1975
|
const path = `./ghfind-export-${ts}.${ext}`;
|
|
1725
|
-
|
|
1976
|
+
writeFileSync2(path, format(repos, fmt), "utf-8");
|
|
1726
1977
|
return path;
|
|
1727
1978
|
}
|
|
1728
1979
|
async function pipeExec(repos, target) {
|
|
@@ -1880,17 +2131,18 @@ async function fetchOrgProfile(org, options = {}) {
|
|
|
1880
2131
|
if (orgPayload.public_repos > 0) {
|
|
1881
2132
|
const perPage = Math.min(limit, 100);
|
|
1882
2133
|
const maxPages = Math.ceil(limit / perPage);
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2134
|
+
const pages = await Promise.all(
|
|
2135
|
+
Array.from({ length: maxPages }, (_, i) => i + 1).map(async (page) => {
|
|
2136
|
+
const url = `https://api.github.com/orgs/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
|
|
2137
|
+
const res = await fetch(url, { headers: apiHeaders(token) });
|
|
2138
|
+
if (!res.ok) {
|
|
2139
|
+
return [];
|
|
2140
|
+
}
|
|
2141
|
+
const items = await res.json();
|
|
2142
|
+
return Array.isArray(items) ? items : [];
|
|
2143
|
+
})
|
|
2144
|
+
);
|
|
2145
|
+
repos.push(...pages.flat());
|
|
1894
2146
|
}
|
|
1895
2147
|
return normalizeOrg(orgPayload, repos.slice(0, limit));
|
|
1896
2148
|
}
|
|
@@ -2094,17 +2346,18 @@ async function fetchUserProfile(user, options = {}) {
|
|
|
2094
2346
|
if (userPayload.public_repos > 0) {
|
|
2095
2347
|
const perPage = Math.min(limit, 100);
|
|
2096
2348
|
const maxPages = Math.ceil(limit / perPage);
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2349
|
+
const pages = await Promise.all(
|
|
2350
|
+
Array.from({ length: maxPages }, (_, i) => i + 1).map(async (page) => {
|
|
2351
|
+
const url = `https://api.github.com/users/${encodeURIComponent(name)}/repos?per_page=${perPage}&sort=pushed&page=${page}`;
|
|
2352
|
+
const res = await fetch(url, { headers: apiHeaders2(token) });
|
|
2353
|
+
if (!res.ok) {
|
|
2354
|
+
return [];
|
|
2355
|
+
}
|
|
2356
|
+
const items = await res.json();
|
|
2357
|
+
return Array.isArray(items) ? items : [];
|
|
2358
|
+
})
|
|
2359
|
+
);
|
|
2360
|
+
repos.push(...pages.flat());
|
|
2108
2361
|
}
|
|
2109
2362
|
return normalizeUser(userPayload, repos.slice(0, limit));
|
|
2110
2363
|
}
|
|
@@ -2220,34 +2473,48 @@ function formatUserMarkdown(p) {
|
|
|
2220
2473
|
|
|
2221
2474
|
// src/storage.ts
|
|
2222
2475
|
import {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
writeFileSync as writeFileSync2,
|
|
2476
|
+
mkdirSync as mkdirSync3,
|
|
2477
|
+
readFileSync as readFileSync5,
|
|
2478
|
+
writeFileSync as writeFileSync3,
|
|
2227
2479
|
appendFileSync,
|
|
2228
|
-
unlinkSync
|
|
2480
|
+
unlinkSync,
|
|
2481
|
+
renameSync
|
|
2229
2482
|
} from "fs";
|
|
2230
|
-
import { join as
|
|
2483
|
+
import { join as join5 } from "path";
|
|
2231
2484
|
function ensureDir() {
|
|
2232
|
-
|
|
2485
|
+
mkdirSync3(stateDir(), { recursive: true });
|
|
2233
2486
|
}
|
|
2234
2487
|
function readJSON(filename, fallback) {
|
|
2235
2488
|
ensureDir();
|
|
2236
2489
|
try {
|
|
2237
|
-
return JSON.parse(
|
|
2490
|
+
return JSON.parse(readFileSync5(join5(stateDir(), filename), "utf-8"));
|
|
2238
2491
|
} catch {
|
|
2239
2492
|
return fallback;
|
|
2240
2493
|
}
|
|
2241
2494
|
}
|
|
2242
|
-
function
|
|
2495
|
+
function writeFileAtomic(filename, data) {
|
|
2243
2496
|
ensureDir();
|
|
2244
|
-
|
|
2497
|
+
const target = join5(stateDir(), filename);
|
|
2498
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
2499
|
+
try {
|
|
2500
|
+
writeFileSync3(tmp, data, "utf-8");
|
|
2501
|
+
renameSync(tmp, target);
|
|
2502
|
+
} catch (e) {
|
|
2503
|
+
try {
|
|
2504
|
+
unlinkSync(tmp);
|
|
2505
|
+
} catch {
|
|
2506
|
+
}
|
|
2507
|
+
throw e;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
function writeJSON(filename, data) {
|
|
2511
|
+
writeFileAtomic(filename, JSON.stringify(data, null, 2));
|
|
2245
2512
|
}
|
|
2246
2513
|
function appendJSONL(filename, obj) {
|
|
2247
2514
|
ensureDir();
|
|
2248
2515
|
try {
|
|
2249
2516
|
appendFileSync(
|
|
2250
|
-
|
|
2517
|
+
join5(stateDir(), filename),
|
|
2251
2518
|
`${JSON.stringify(obj)}
|
|
2252
2519
|
`,
|
|
2253
2520
|
"utf-8"
|
|
@@ -2258,7 +2525,7 @@ function appendJSONL(filename, obj) {
|
|
|
2258
2525
|
function readJSONL(filename) {
|
|
2259
2526
|
ensureDir();
|
|
2260
2527
|
try {
|
|
2261
|
-
const raw =
|
|
2528
|
+
const raw = readFileSync5(join5(stateDir(), filename), "utf-8").trim();
|
|
2262
2529
|
if (!raw) return [];
|
|
2263
2530
|
return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
2264
2531
|
} catch {
|
|
@@ -2266,14 +2533,13 @@ function readJSONL(filename) {
|
|
|
2266
2533
|
}
|
|
2267
2534
|
}
|
|
2268
2535
|
function writeRaw(filename, data) {
|
|
2269
|
-
|
|
2270
|
-
writeFileSync2(join3(stateDir(), filename), data);
|
|
2536
|
+
writeFileAtomic(filename, data);
|
|
2271
2537
|
}
|
|
2272
2538
|
function debugLog(msg) {
|
|
2273
2539
|
if (!process.env.DEBUG) return;
|
|
2274
2540
|
try {
|
|
2275
2541
|
appendFileSync(
|
|
2276
|
-
|
|
2542
|
+
join5(stateDir(), "ghfind-debug.log"),
|
|
2277
2543
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
2278
2544
|
`,
|
|
2279
2545
|
"utf-8"
|
|
@@ -2330,9 +2596,12 @@ function markSeen(fullName) {
|
|
|
2330
2596
|
// src/notifications.ts
|
|
2331
2597
|
var nextId = 1;
|
|
2332
2598
|
var notifs = [];
|
|
2599
|
+
var loaded = false;
|
|
2333
2600
|
var MAX_NOTIFS = 50;
|
|
2334
2601
|
var FILE = "notifications.json";
|
|
2335
|
-
function
|
|
2602
|
+
function ensureLoaded() {
|
|
2603
|
+
if (loaded) return;
|
|
2604
|
+
loaded = true;
|
|
2336
2605
|
notifs = readJSON(FILE, []);
|
|
2337
2606
|
nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
|
|
2338
2607
|
}
|
|
@@ -2340,7 +2609,7 @@ function save() {
|
|
|
2340
2609
|
writeJSON(FILE, notifs);
|
|
2341
2610
|
}
|
|
2342
2611
|
function addNotification(type, message) {
|
|
2343
|
-
|
|
2612
|
+
ensureLoaded();
|
|
2344
2613
|
const n = {
|
|
2345
2614
|
id: nextId++,
|
|
2346
2615
|
type,
|
|
@@ -2370,14 +2639,16 @@ function iconFor(type) {
|
|
|
2370
2639
|
}
|
|
2371
2640
|
}
|
|
2372
2641
|
function getNotifications() {
|
|
2373
|
-
|
|
2642
|
+
ensureLoaded();
|
|
2374
2643
|
return [...notifs];
|
|
2375
2644
|
}
|
|
2376
2645
|
function dismissNotification(id) {
|
|
2646
|
+
ensureLoaded();
|
|
2377
2647
|
notifs = notifs.filter((n) => n.id !== id);
|
|
2378
2648
|
save();
|
|
2379
2649
|
}
|
|
2380
2650
|
function dismissAll() {
|
|
2651
|
+
ensureLoaded();
|
|
2381
2652
|
notifs = [];
|
|
2382
2653
|
save();
|
|
2383
2654
|
}
|
|
@@ -2385,6 +2656,7 @@ function dismissAll() {
|
|
|
2385
2656
|
// src/releases.ts
|
|
2386
2657
|
var RELEASES_FILE = "release-cache.json";
|
|
2387
2658
|
var PER_REPO = 5;
|
|
2659
|
+
var CONCURRENCY = 6;
|
|
2388
2660
|
function loadCache() {
|
|
2389
2661
|
return readJSON(RELEASES_FILE, {});
|
|
2390
2662
|
}
|
|
@@ -2424,43 +2696,31 @@ async function checkReleases(opts = {}) {
|
|
|
2424
2696
|
const config = loadConfig();
|
|
2425
2697
|
const token = opts.token ?? config.githubToken ?? process.env.GITHUB_TOKEN;
|
|
2426
2698
|
const results2 = [];
|
|
2427
|
-
|
|
2699
|
+
const checkOne = async (fullName) => {
|
|
2428
2700
|
try {
|
|
2429
|
-
const prev = cache2[
|
|
2430
|
-
const lastSeen = getLastSeen(
|
|
2701
|
+
const prev = cache2[fullName];
|
|
2702
|
+
const lastSeen = getLastSeen(fullName);
|
|
2431
2703
|
const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
|
|
2432
2704
|
const { status, etag, releases } = await fetchReleases(
|
|
2433
|
-
|
|
2705
|
+
fullName,
|
|
2434
2706
|
prev?.etag,
|
|
2435
2707
|
token
|
|
2436
2708
|
);
|
|
2437
2709
|
if (status === 304 && prev) {
|
|
2438
|
-
|
|
2439
|
-
fullName: bm.repo.fullName,
|
|
2440
|
-
newReleases: prev.releases.filter(isNew)
|
|
2441
|
-
});
|
|
2442
|
-
continue;
|
|
2710
|
+
return { fullName, newReleases: prev.releases.filter(isNew) };
|
|
2443
2711
|
}
|
|
2444
2712
|
if (status !== 200 || !releases) {
|
|
2445
|
-
|
|
2446
|
-
fullName: bm.repo.fullName,
|
|
2447
|
-
newReleases: [],
|
|
2448
|
-
error: `HTTP ${status}`
|
|
2449
|
-
});
|
|
2450
|
-
continue;
|
|
2713
|
+
return { fullName, newReleases: [], error: `HTTP ${status}` };
|
|
2451
2714
|
}
|
|
2452
|
-
cache2[
|
|
2453
|
-
|
|
2454
|
-
fullName: bm.repo.fullName,
|
|
2455
|
-
newReleases: releases.filter(isNew)
|
|
2456
|
-
});
|
|
2715
|
+
cache2[fullName] = { checkedAt: Date.now(), etag, releases };
|
|
2716
|
+
return { fullName, newReleases: releases.filter(isNew) };
|
|
2457
2717
|
} catch (err) {
|
|
2458
|
-
|
|
2459
|
-
fullName: bm.repo.fullName,
|
|
2460
|
-
newReleases: [],
|
|
2461
|
-
error: err.message
|
|
2462
|
-
});
|
|
2718
|
+
return { fullName, newReleases: [], error: err.message };
|
|
2463
2719
|
}
|
|
2720
|
+
};
|
|
2721
|
+
for (let i = 0; i < bookmarks.length; i += CONCURRENCY) {
|
|
2722
|
+
const batch = bookmarks.slice(i, i + CONCURRENCY).map((bm) => checkOne(bm.repo.fullName));
|
|
2723
|
+
results2.push(...await Promise.all(batch));
|
|
2464
2724
|
}
|
|
2465
2725
|
saveCache(cache2);
|
|
2466
2726
|
return results2;
|
|
@@ -2480,144 +2740,17 @@ async function checkAndNotify(opts = {}) {
|
|
|
2480
2740
|
}
|
|
2481
2741
|
function allCachedReleases() {
|
|
2482
2742
|
const cache2 = loadCache();
|
|
2483
|
-
const
|
|
2484
|
-
return Object.entries(cache2).filter(([fullName]) =>
|
|
2743
|
+
const bookmarked = new Set(getBookmarks().map((b) => b.repo.fullName));
|
|
2744
|
+
return Object.entries(cache2).filter(([fullName]) => bookmarked.has(fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
|
|
2485
2745
|
(a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
|
|
2486
2746
|
);
|
|
2487
2747
|
}
|
|
2488
2748
|
|
|
2489
2749
|
// src/init.ts
|
|
2490
2750
|
import { createInterface as createInterface2 } from "readline";
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
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
|
-
function ask(query) {
|
|
2619
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2620
|
-
return new Promise((resolve) => {
|
|
2751
|
+
function ask(query) {
|
|
2752
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2753
|
+
return new Promise((resolve) => {
|
|
2621
2754
|
rl.question(query, (answer) => {
|
|
2622
2755
|
rl.close();
|
|
2623
2756
|
resolve(answer.trim());
|
|
@@ -2650,8 +2783,7 @@ async function runInitWizard() {
|
|
|
2650
2783
|
};
|
|
2651
2784
|
if (token) config.githubToken = token;
|
|
2652
2785
|
saveConfig(config);
|
|
2653
|
-
|
|
2654
|
-
console.log(` \u2713 Config saved to ${configPath2()}`);
|
|
2786
|
+
console.log(` \u2713 Config saved to ${configPath()}`);
|
|
2655
2787
|
console.log(" Run 'ghfind' to start browsing!");
|
|
2656
2788
|
console.log("");
|
|
2657
2789
|
}
|
|
@@ -2755,6 +2887,336 @@ function comparisonMarkdown(repos) {
|
|
|
2755
2887
|
return [header, sep, ...rows].join("\n");
|
|
2756
2888
|
}
|
|
2757
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
|
+
|
|
3006
|
+
// src/image-loader.ts
|
|
3007
|
+
var MAX_IMAGE_BYTES = 6 * 1024 * 1024;
|
|
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;
|
|
3009
|
+
var RAW_HOSTS = /* @__PURE__ */ new Set([
|
|
3010
|
+
"raw.githubusercontent.com",
|
|
3011
|
+
"camo.githubusercontent.com",
|
|
3012
|
+
"user-images.githubusercontent.com",
|
|
3013
|
+
"private-user-images.githubusercontent.com",
|
|
3014
|
+
"avatars.githubusercontent.com",
|
|
3015
|
+
"objects.githubusercontent.com"
|
|
3016
|
+
]);
|
|
3017
|
+
var RASTER_EXT = /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)(?:[?#]|$)/i;
|
|
3018
|
+
var CACHE_LIMIT = 24;
|
|
3019
|
+
var MAX_CONCURRENT_IMAGE_FETCHES = 4;
|
|
3020
|
+
var cache = /* @__PURE__ */ new Map();
|
|
3021
|
+
var activeFetches = 0;
|
|
3022
|
+
var waiting = [];
|
|
3023
|
+
function releaseSlot() {
|
|
3024
|
+
activeFetches--;
|
|
3025
|
+
waiting.shift()?.();
|
|
3026
|
+
}
|
|
3027
|
+
async function withSlot(job) {
|
|
3028
|
+
if (activeFetches >= MAX_CONCURRENT_IMAGE_FETCHES) {
|
|
3029
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
3030
|
+
}
|
|
3031
|
+
activeFetches++;
|
|
3032
|
+
try {
|
|
3033
|
+
return await job();
|
|
3034
|
+
} finally {
|
|
3035
|
+
releaseSlot();
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
function decodeDataUri(uri) {
|
|
3039
|
+
const match = /^data:([^;,]+)?(;base64)?,(.*)$/is.exec(uri);
|
|
3040
|
+
if (!match) return null;
|
|
3041
|
+
const mime = (match[1] ?? "").toLowerCase();
|
|
3042
|
+
if (mime.includes("svg")) return null;
|
|
3043
|
+
const payload = match[3];
|
|
3044
|
+
try {
|
|
3045
|
+
if (match[2]) return new Uint8Array(Buffer.from(payload, "base64"));
|
|
3046
|
+
return new Uint8Array(Buffer.from(decodeURIComponent(payload), "binary"));
|
|
3047
|
+
} catch {
|
|
3048
|
+
return null;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
function toRawGitHubUrl(url) {
|
|
3052
|
+
if (url.hostname.toLowerCase() !== "github.com") return url;
|
|
3053
|
+
const match = /^\/([^/]+)\/([^/]+)\/(?:blob|raw)\/(.+)$/.exec(url.pathname);
|
|
3054
|
+
if (!match) return url;
|
|
3055
|
+
const [, owner, repo, rest] = match;
|
|
3056
|
+
return new URL(
|
|
3057
|
+
`https://raw.githubusercontent.com/${owner}/${repo}/${rest}${url.search}`
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
function resolveRootRelative(path, base) {
|
|
3061
|
+
let parsed;
|
|
3062
|
+
try {
|
|
3063
|
+
parsed = new URL(base);
|
|
3064
|
+
} catch {
|
|
3065
|
+
return null;
|
|
3066
|
+
}
|
|
3067
|
+
if (parsed.hostname.toLowerCase() !== "raw.githubusercontent.com") {
|
|
3068
|
+
return null;
|
|
3069
|
+
}
|
|
3070
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
3071
|
+
if (segments.length < 3) return null;
|
|
3072
|
+
return new URL(`${parsed.origin}/${segments.slice(0, 3).join("/")}${path}`);
|
|
3073
|
+
}
|
|
3074
|
+
function resolveReadmeImageUrl(raw, base) {
|
|
3075
|
+
const cleaned = raw.trim();
|
|
3076
|
+
if (!cleaned) return null;
|
|
3077
|
+
if (/^data:/i.test(cleaned)) {
|
|
3078
|
+
return isRenderableImageUrl(cleaned) ? cleaned : null;
|
|
3079
|
+
}
|
|
3080
|
+
let url = null;
|
|
3081
|
+
try {
|
|
3082
|
+
url = cleaned.startsWith("/") ? resolveRootRelative(cleaned, base) : new URL(cleaned, base);
|
|
3083
|
+
} catch {
|
|
3084
|
+
return null;
|
|
3085
|
+
}
|
|
3086
|
+
if (!url) return null;
|
|
3087
|
+
url = toRawGitHubUrl(url);
|
|
3088
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
3089
|
+
url.hash = "";
|
|
3090
|
+
const resolved = url.toString();
|
|
3091
|
+
return isRenderableImageUrl(resolved) ? resolved : null;
|
|
3092
|
+
}
|
|
3093
|
+
function isRenderableImageUrl(url) {
|
|
3094
|
+
if (/^data:/i.test(url)) return !/svg/i.test(url.slice(0, 64));
|
|
3095
|
+
let parsed;
|
|
3096
|
+
try {
|
|
3097
|
+
parsed = new URL(url);
|
|
3098
|
+
} catch {
|
|
3099
|
+
return false;
|
|
3100
|
+
}
|
|
3101
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
3102
|
+
if (BADGE_HOST.test(parsed.hostname)) return false;
|
|
3103
|
+
const path = parsed.pathname.toLowerCase();
|
|
3104
|
+
if (path.endsWith(".svg") || path.endsWith(".svgz")) return false;
|
|
3105
|
+
if (parsed.hostname.toLowerCase().endsWith("github.com")) {
|
|
3106
|
+
if (/^\/user-attachments\//.test(path)) return true;
|
|
3107
|
+
if (!RAW_HOSTS.has(parsed.hostname)) {
|
|
3108
|
+
return /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)$/i.test(path);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
if (/\.[a-z0-9]{1,5}$/.test(path) && !RASTER_EXT.test(path)) return false;
|
|
3112
|
+
return true;
|
|
3113
|
+
}
|
|
3114
|
+
function isGitHubHost(hostname) {
|
|
3115
|
+
return /(^|\.)githubusercontent\.com$|(^|\.)github\.com$/i.test(hostname);
|
|
3116
|
+
}
|
|
3117
|
+
async function fetchImageBytes(url, opts = {}) {
|
|
3118
|
+
const cached2 = cache.get(url);
|
|
3119
|
+
if (cached2) return cached2;
|
|
3120
|
+
const pending = withSlot(() => loadImageBytes(url, opts));
|
|
3121
|
+
cache.set(url, pending);
|
|
3122
|
+
while (cache.size > CACHE_LIMIT) {
|
|
3123
|
+
const oldest = cache.keys().next().value;
|
|
3124
|
+
if (oldest === void 0) break;
|
|
3125
|
+
cache.delete(oldest);
|
|
3126
|
+
}
|
|
3127
|
+
return pending;
|
|
3128
|
+
}
|
|
3129
|
+
async function loadImageBytes(url, opts) {
|
|
3130
|
+
if (/^data:/i.test(url)) return decodeDataUri(url);
|
|
3131
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
3132
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
3133
|
+
const controller = new AbortController();
|
|
3134
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3135
|
+
try {
|
|
3136
|
+
const headers = {
|
|
3137
|
+
"User-Agent": "ghfind/1.0",
|
|
3138
|
+
Accept: "image/*,*/*;q=0.5"
|
|
3139
|
+
};
|
|
3140
|
+
let host = "";
|
|
3141
|
+
try {
|
|
3142
|
+
host = new URL(url).hostname;
|
|
3143
|
+
} catch {
|
|
3144
|
+
return null;
|
|
3145
|
+
}
|
|
3146
|
+
if (opts.token && isGitHubHost(host)) {
|
|
3147
|
+
headers.Authorization = `Bearer ${opts.token}`;
|
|
3148
|
+
}
|
|
3149
|
+
const res = await fetchImpl(url, { headers, signal: controller.signal });
|
|
3150
|
+
if (!res.ok) return null;
|
|
3151
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
3152
|
+
if (contentType && !/^image\//i.test(contentType)) {
|
|
3153
|
+
const githubHost = isGitHubHost(host);
|
|
3154
|
+
if (!githubHost || !/octet-stream/i.test(contentType)) return null;
|
|
3155
|
+
}
|
|
3156
|
+
if (/svg/i.test(contentType)) return null;
|
|
3157
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
3158
|
+
if (declared > MAX_IMAGE_BYTES) return null;
|
|
3159
|
+
const buffer = new Uint8Array(await res.arrayBuffer());
|
|
3160
|
+
if (buffer.byteLength === 0 || buffer.byteLength > MAX_IMAGE_BYTES) {
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
return buffer;
|
|
3164
|
+
} catch {
|
|
3165
|
+
return null;
|
|
3166
|
+
} finally {
|
|
3167
|
+
clearTimeout(timer);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
function readmeBaseUrl(owner, repo, branch, path = "README.md") {
|
|
3171
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
|
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
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// src/readme.ts
|
|
3192
|
+
var CANDIDATES = [
|
|
3193
|
+
["main", "README.md"],
|
|
3194
|
+
["master", "README.md"],
|
|
3195
|
+
["main", "README.rst"]
|
|
3196
|
+
];
|
|
3197
|
+
function buildReadmeHeaders(url, token, isPrivate = false) {
|
|
3198
|
+
const headers = { "User-Agent": "ghfind/1.0" };
|
|
3199
|
+
if (token && isPrivate && new URL(url).origin === "https://api.github.com") {
|
|
3200
|
+
headers.Authorization = `Bearer ${token}`;
|
|
3201
|
+
}
|
|
3202
|
+
return headers;
|
|
3203
|
+
}
|
|
3204
|
+
async function fetchReadme(owner, repo, opts = {}) {
|
|
3205
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
3206
|
+
const attempts = CANDIDATES.map(async ([branch, file]) => {
|
|
3207
|
+
const url = readmeBaseUrl(owner, repo, branch, file);
|
|
3208
|
+
const headers = buildReadmeHeaders(url, opts.token, opts.private);
|
|
3209
|
+
const res = await fetchImpl(url, { headers });
|
|
3210
|
+
if (!res.ok) throw new Error(`no readme at ${url}`);
|
|
3211
|
+
return { text: await res.text(), sourceUrl: url };
|
|
3212
|
+
});
|
|
3213
|
+
try {
|
|
3214
|
+
return await Promise.any(attempts);
|
|
3215
|
+
} catch {
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
|
|
2758
3220
|
// src/deepdive.ts
|
|
2759
3221
|
var USER_AGENT = "ghfind/1.0";
|
|
2760
3222
|
function buildHeaders(token) {
|
|
@@ -2799,42 +3261,46 @@ function repoFromRef(fullName) {
|
|
|
2799
3261
|
async function resolveRepoFromRef(fullName, token) {
|
|
2800
3262
|
const ref = parseRepoRef(fullName);
|
|
2801
3263
|
const canonical = `${ref.owner}/${ref.name}`;
|
|
3264
|
+
let res;
|
|
2802
3265
|
try {
|
|
2803
|
-
|
|
3266
|
+
res = await fetch(`https://api.github.com/repos/${canonical}`, {
|
|
2804
3267
|
headers: buildHeaders(token)
|
|
2805
3268
|
});
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
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
|
-
}
|
|
3269
|
+
} catch {
|
|
3270
|
+
return repoFromRef(canonical);
|
|
3271
|
+
}
|
|
3272
|
+
if (res.status === 404) {
|
|
3273
|
+
throw new Error(`Repo not found: ${canonical}`);
|
|
3274
|
+
}
|
|
3275
|
+
if (!res.ok) {
|
|
3276
|
+
throw new Error(`GitHub API error ${res.status} for ${canonical}`);
|
|
3277
|
+
}
|
|
3278
|
+
let api;
|
|
3279
|
+
try {
|
|
3280
|
+
api = await res.json();
|
|
3281
|
+
} catch {
|
|
3282
|
+
return repoFromRef(canonical);
|
|
2836
3283
|
}
|
|
2837
|
-
return
|
|
3284
|
+
return {
|
|
3285
|
+
id: 0,
|
|
3286
|
+
fullName: canonical,
|
|
3287
|
+
name: ref.name,
|
|
3288
|
+
owner: ref.owner,
|
|
3289
|
+
description: api.description,
|
|
3290
|
+
url: api.html_url,
|
|
3291
|
+
stars: api.stargazers_count ?? 0,
|
|
3292
|
+
forks: api.forks_count ?? 0,
|
|
3293
|
+
watchers: api.subscribers_count ?? 0,
|
|
3294
|
+
language: api.language,
|
|
3295
|
+
topics: api.topics ?? [],
|
|
3296
|
+
archived: api.archived ?? false,
|
|
3297
|
+
isFork: api.fork ?? false,
|
|
3298
|
+
private: api.private ?? false,
|
|
3299
|
+
createdAt: api.created_at ?? "",
|
|
3300
|
+
updatedAt: api.updated_at ?? "",
|
|
3301
|
+
pushedAt: api.pushed_at ?? "",
|
|
3302
|
+
score: 0
|
|
3303
|
+
};
|
|
2838
3304
|
}
|
|
2839
3305
|
var README_MAX_CHARS = 5e3;
|
|
2840
3306
|
async function fetchDeepDiveRaw(repo, token) {
|
|
@@ -2848,20 +3314,11 @@ async function fetchDeepDiveRaw(repo, token) {
|
|
|
2848
3314
|
{ headers }
|
|
2849
3315
|
)
|
|
2850
3316
|
]);
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
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
|
-
}
|
|
3317
|
+
const readme = await fetchReadme(repo.owner, repo.name, {
|
|
3318
|
+
token: repo.private ? token : void 0,
|
|
3319
|
+
private: repo.private
|
|
3320
|
+
});
|
|
3321
|
+
const readmeText = readme?.text ?? "";
|
|
2865
3322
|
const langEntries = languagesRes.ok ? Object.entries(
|
|
2866
3323
|
await languagesRes.json().catch(() => ({}))
|
|
2867
3324
|
) : [];
|
|
@@ -2952,8 +3409,8 @@ function buildDeepDiveText(data) {
|
|
|
2952
3409
|
}
|
|
2953
3410
|
|
|
2954
3411
|
// src/cli.ts
|
|
2955
|
-
import { readFileSync as
|
|
2956
|
-
import { join as
|
|
3412
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3413
|
+
import { join as join8, dirname as dirname4 } from "path";
|
|
2957
3414
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2958
3415
|
|
|
2959
3416
|
// src/tui.ts
|
|
@@ -3038,6 +3495,17 @@ function buildHelpSections() {
|
|
|
3038
3495
|
{ keys: "PageUp", action: "Scroll to top of results" }
|
|
3039
3496
|
]
|
|
3040
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
|
+
},
|
|
3041
3509
|
{
|
|
3042
3510
|
title: " Trending Mode ",
|
|
3043
3511
|
titleColor: "yellow",
|
|
@@ -3176,13 +3644,10 @@ function readHistory() {
|
|
|
3176
3644
|
function deleteHistoryEntry(index) {
|
|
3177
3645
|
const entries = readHistory();
|
|
3178
3646
|
if (index < 0 || index >= entries.length) return;
|
|
3179
|
-
|
|
3180
|
-
const remaining = entries.filter(
|
|
3181
|
-
(e) => e.timestamp !== removed.timestamp || e.query !== removed.query
|
|
3182
|
-
);
|
|
3647
|
+
entries.splice(index, 1);
|
|
3183
3648
|
writeRaw(
|
|
3184
3649
|
HISTORY_FILE,
|
|
3185
|
-
`${
|
|
3650
|
+
`${entries.reverse().map((e) => JSON.stringify(e)).join("\n")}
|
|
3186
3651
|
`
|
|
3187
3652
|
);
|
|
3188
3653
|
}
|
|
@@ -3253,9 +3718,35 @@ function touchSavedSearch(name) {
|
|
|
3253
3718
|
|
|
3254
3719
|
// src/session.ts
|
|
3255
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
|
+
}
|
|
3256
3747
|
function saveSession(state) {
|
|
3257
3748
|
try {
|
|
3258
|
-
writeJSON(SESSION_FILE, state);
|
|
3749
|
+
writeJSON(SESSION_FILE, sanitizeSession(state));
|
|
3259
3750
|
} catch (err) {
|
|
3260
3751
|
if (process.env.DEBUG)
|
|
3261
3752
|
console.error(
|
|
@@ -3264,7 +3755,7 @@ function saveSession(state) {
|
|
|
3264
3755
|
}
|
|
3265
3756
|
}
|
|
3266
3757
|
function restoreSession() {
|
|
3267
|
-
return readJSON(SESSION_FILE, null);
|
|
3758
|
+
return sanitizeSession(readJSON(SESSION_FILE, null));
|
|
3268
3759
|
}
|
|
3269
3760
|
|
|
3270
3761
|
// src/markdown-view.ts
|
|
@@ -3505,314 +3996,69 @@ function fitImageToCells(imgWidth, imgHeight, opts) {
|
|
|
3505
3996
|
const rowsPerCol = cellAspect * (imgHeight / imgWidth);
|
|
3506
3997
|
let cols = Math.max(minCols, Math.floor(opts.maxCols));
|
|
3507
3998
|
let rows = Math.max(1, Math.round(cols * rowsPerCol));
|
|
3508
|
-
if (rows > opts.maxRows) {
|
|
3509
|
-
rows = Math.max(1, opts.maxRows);
|
|
3510
|
-
cols = Math.max(
|
|
3511
|
-
minCols,
|
|
3512
|
-
Math.min(cols, Math.floor(rows / Math.max(rowsPerCol, 1e-6)))
|
|
3513
|
-
);
|
|
3514
|
-
}
|
|
3515
|
-
return { cols, rows };
|
|
3516
|
-
}
|
|
3517
|
-
function pixelAt(img, x, y, background) {
|
|
3518
|
-
const o = (y * img.width + x) * 4;
|
|
3519
|
-
const a = img.data[o + 3] / 255;
|
|
3520
|
-
if (a >= 1) return [img.data[o], img.data[o + 1], img.data[o + 2]];
|
|
3521
|
-
const mix = (channel, bg) => Math.round(channel * a + bg * (1 - a));
|
|
3522
|
-
return [
|
|
3523
|
-
mix(img.data[o], background[0]),
|
|
3524
|
-
mix(img.data[o + 1], background[1]),
|
|
3525
|
-
mix(img.data[o + 2], background[2])
|
|
3526
|
-
];
|
|
3527
|
-
}
|
|
3528
|
-
function toHalfBlocks(img, opts = {}) {
|
|
3529
|
-
const background = opts.background ?? [0, 0, 0];
|
|
3530
|
-
const cells = [];
|
|
3531
|
-
for (let row = 0; row * 2 < img.height; row++) {
|
|
3532
|
-
const cellsRow = [];
|
|
3533
|
-
const topY = row * 2;
|
|
3534
|
-
const bottomY = topY + 1;
|
|
3535
|
-
for (let x = 0; x < img.width; x++) {
|
|
3536
|
-
const top = pixelAt(img, x, topY, background);
|
|
3537
|
-
const bottom = bottomY < img.height ? pixelAt(img, x, bottomY, background) : top;
|
|
3538
|
-
cellsRow.push({ top, bottom });
|
|
3539
|
-
}
|
|
3540
|
-
cells.push(cellsRow);
|
|
3541
|
-
}
|
|
3542
|
-
return { cols: img.width, rows: cells.length, cells };
|
|
3543
|
-
}
|
|
3544
|
-
function toImageRuns(image) {
|
|
3545
|
-
return image.cells.map((cells) => {
|
|
3546
|
-
const runs = [];
|
|
3547
|
-
for (const cell of cells) {
|
|
3548
|
-
const last = runs[runs.length - 1];
|
|
3549
|
-
if (last && last.top[0] === cell.top[0] && last.top[1] === cell.top[1] && last.top[2] === cell.top[2] && last.bottom[0] === cell.bottom[0] && last.bottom[1] === cell.bottom[1] && last.bottom[2] === cell.bottom[2]) {
|
|
3550
|
-
last.text += HALF_BLOCK;
|
|
3551
|
-
continue;
|
|
3552
|
-
}
|
|
3553
|
-
runs.push({ text: HALF_BLOCK, top: cell.top, bottom: cell.bottom });
|
|
3554
|
-
}
|
|
3555
|
-
return runs;
|
|
3556
|
-
});
|
|
3557
|
-
}
|
|
3558
|
-
var MAX_SOURCE_PIXELS = 2e7;
|
|
3559
|
-
async function rasterizeImage(bytes, opts) {
|
|
3560
|
-
const ImageCtor = globalThis.Bun?.Image;
|
|
3561
|
-
if (!ImageCtor) return null;
|
|
3562
|
-
const source = new ImageCtor(bytes, { maxPixels: MAX_SOURCE_PIXELS });
|
|
3563
|
-
try {
|
|
3564
|
-
const meta = await source.metadata();
|
|
3565
|
-
const { cols, rows } = fitImageToCells(meta.width, meta.height, opts);
|
|
3566
|
-
const png = await source.resize(cols, rows * 2, { fit: "fill" }).png({ compressionLevel: 1 }).bytes();
|
|
3567
|
-
return toHalfBlocks(decodePng(png), { background: opts.background });
|
|
3568
|
-
} catch {
|
|
3569
|
-
return null;
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
|
|
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);
|
|
3999
|
+
if (rows > opts.maxRows) {
|
|
4000
|
+
rows = Math.max(1, opts.maxRows);
|
|
4001
|
+
cols = Math.max(
|
|
4002
|
+
minCols,
|
|
4003
|
+
Math.min(cols, Math.floor(rows / Math.max(rowsPerCol, 1e-6)))
|
|
4004
|
+
);
|
|
3735
4005
|
}
|
|
4006
|
+
return { cols, rows };
|
|
3736
4007
|
}
|
|
3737
|
-
function
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
const withoutTags = raw.replace(IMG_TAG, "").replace(/<\/?(?:p|div|center|br|a|picture|source)\b[^>]*>/gi, "").trim();
|
|
3748
|
-
if (withoutTags) return [];
|
|
3749
|
-
const refs = [];
|
|
3750
|
-
for (const match of raw.matchAll(IMG_TAG)) {
|
|
3751
|
-
const tag = match[0];
|
|
3752
|
-
const alt = ALT_ATTR.exec(tag)?.[1] ?? "";
|
|
3753
|
-
refs.push({ href: match[1], alt });
|
|
3754
|
-
}
|
|
3755
|
-
return refs;
|
|
4008
|
+
function pixelAt(img, x, y, background) {
|
|
4009
|
+
const o = (y * img.width + x) * 4;
|
|
4010
|
+
const a = img.data[o + 3] / 255;
|
|
4011
|
+
if (a >= 1) return [img.data[o], img.data[o + 1], img.data[o + 2]];
|
|
4012
|
+
const mix = (channel, bg) => Math.round(channel * a + bg * (1 - a));
|
|
4013
|
+
return [
|
|
4014
|
+
mix(img.data[o], background[0]),
|
|
4015
|
+
mix(img.data[o + 1], background[1]),
|
|
4016
|
+
mix(img.data[o + 2], background[2])
|
|
4017
|
+
];
|
|
3756
4018
|
}
|
|
3757
|
-
function
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
for (
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
continue;
|
|
3769
|
-
}
|
|
3770
|
-
if (token.text && (!token.tokens || token.tokens.length === 0)) {
|
|
3771
|
-
out += token.text;
|
|
3772
|
-
continue;
|
|
3773
|
-
}
|
|
3774
|
-
if (token.tokens?.length) {
|
|
3775
|
-
out += inlinePlainText(token.tokens);
|
|
3776
|
-
continue;
|
|
4019
|
+
function toHalfBlocks(img, opts = {}) {
|
|
4020
|
+
const background = opts.background ?? [0, 0, 0];
|
|
4021
|
+
const cells = [];
|
|
4022
|
+
for (let row = 0; row * 2 < img.height; row++) {
|
|
4023
|
+
const cellsRow = [];
|
|
4024
|
+
const topY = row * 2;
|
|
4025
|
+
const bottomY = topY + 1;
|
|
4026
|
+
for (let x = 0; x < img.width; x++) {
|
|
4027
|
+
const top = pixelAt(img, x, topY, background);
|
|
4028
|
+
const bottom = bottomY < img.height ? pixelAt(img, x, bottomY, background) : top;
|
|
4029
|
+
cellsRow.push({ top, bottom });
|
|
3777
4030
|
}
|
|
3778
|
-
|
|
4031
|
+
cells.push(cellsRow);
|
|
3779
4032
|
}
|
|
3780
|
-
return
|
|
4033
|
+
return { cols: img.width, rows: cells.length, cells };
|
|
3781
4034
|
}
|
|
3782
|
-
function
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
if (inline2.type === "html") {
|
|
3791
|
-
const refs = imagesFromHtml(inline2.raw);
|
|
3792
|
-
if (refs.length > 0) {
|
|
3793
|
-
images.push(...refs);
|
|
3794
|
-
return true;
|
|
4035
|
+
function toImageRuns(image) {
|
|
4036
|
+
return image.cells.map((cells) => {
|
|
4037
|
+
const runs = [];
|
|
4038
|
+
for (const cell of cells) {
|
|
4039
|
+
const last = runs[runs.length - 1];
|
|
4040
|
+
if (last && last.top[0] === cell.top[0] && last.top[1] === cell.top[1] && last.top[2] === cell.top[2] && last.bottom[0] === cell.bottom[0] && last.bottom[1] === cell.bottom[1] && last.bottom[2] === cell.bottom[2]) {
|
|
4041
|
+
last.text += HALF_BLOCK;
|
|
4042
|
+
continue;
|
|
3795
4043
|
}
|
|
3796
|
-
|
|
3797
|
-
}
|
|
3798
|
-
if (inline2.type === "text" || inline2.type === "escape") {
|
|
3799
|
-
return !(inline2.text ?? "").trim();
|
|
4044
|
+
runs.push({ text: HALF_BLOCK, top: cell.top, bottom: cell.bottom });
|
|
3800
4045
|
}
|
|
3801
|
-
return
|
|
3802
|
-
};
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
4046
|
+
return runs;
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
var MAX_SOURCE_PIXELS = 2e7;
|
|
4050
|
+
async function rasterizeImage(bytes, opts) {
|
|
4051
|
+
const ImageCtor = globalThis.Bun?.Image;
|
|
4052
|
+
if (!ImageCtor) return null;
|
|
4053
|
+
const source = new ImageCtor(bytes, { maxPixels: MAX_SOURCE_PIXELS });
|
|
4054
|
+
try {
|
|
4055
|
+
const meta = await source.metadata();
|
|
4056
|
+
const { cols, rows } = fitImageToCells(meta.width, meta.height, opts);
|
|
4057
|
+
const png = await source.resize(cols, rows * 2, { fit: "fill" }).png({ compressionLevel: 1 }).bytes();
|
|
4058
|
+
return toHalfBlocks(decodePng(png), { background: opts.background });
|
|
4059
|
+
} catch {
|
|
4060
|
+
return null;
|
|
3814
4061
|
}
|
|
3815
|
-
return images.length > 0 ? images : null;
|
|
3816
4062
|
}
|
|
3817
4063
|
|
|
3818
4064
|
// src/markdown-view.ts
|
|
@@ -3850,17 +4096,19 @@ function hexToRgb(hex) {
|
|
|
3850
4096
|
function rgba(color) {
|
|
3851
4097
|
return RGBA.fromValues(color[0], color[1], color[2], 255);
|
|
3852
4098
|
}
|
|
3853
|
-
function imageStyledText(image) {
|
|
4099
|
+
function imageStyledText(image, linkUrl) {
|
|
3854
4100
|
const chunks = [];
|
|
3855
4101
|
toImageRuns(image).forEach((runs, rowIndex) => {
|
|
3856
4102
|
if (rowIndex > 0) chunks.push({ __isChunk: true, text: "\n" });
|
|
3857
4103
|
for (const run2 of runs) {
|
|
3858
|
-
|
|
4104
|
+
const chunk = {
|
|
3859
4105
|
__isChunk: true,
|
|
3860
4106
|
text: run2.text,
|
|
3861
4107
|
fg: rgba(run2.top),
|
|
3862
4108
|
bg: rgba(run2.bottom)
|
|
3863
|
-
}
|
|
4109
|
+
};
|
|
4110
|
+
if (linkUrl) chunk.link = { url: linkUrl };
|
|
4111
|
+
chunks.push(chunk);
|
|
3864
4112
|
}
|
|
3865
4113
|
});
|
|
3866
4114
|
return new StyledText3(chunks);
|
|
@@ -3887,15 +4135,21 @@ function createMarkdownView(opts) {
|
|
|
3887
4135
|
tableOptions: { style: "grid" },
|
|
3888
4136
|
width: "100%"
|
|
3889
4137
|
});
|
|
3890
|
-
const caption = (label, color = muted()) => new TextRenderable3(renderer, {
|
|
3891
|
-
content:
|
|
3892
|
-
|
|
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
|
+
]),
|
|
3893
4147
|
bg: bg(),
|
|
3894
4148
|
wrapMode: "word",
|
|
3895
4149
|
flexShrink: 0,
|
|
3896
4150
|
width: "100%"
|
|
3897
4151
|
});
|
|
3898
|
-
const rasterize = (url) => {
|
|
4152
|
+
const rasterize = (url, declared) => {
|
|
3899
4153
|
const maxCols = Math.max(8, Math.floor(opts.getImageWidth?.() ?? 100));
|
|
3900
4154
|
const key = `${maxCols}|${url}`;
|
|
3901
4155
|
const cached2 = rasterCache.get(key);
|
|
@@ -3905,6 +4159,7 @@ function createMarkdownView(opts) {
|
|
|
3905
4159
|
if (!loader) return null;
|
|
3906
4160
|
const bytes = await loader(url);
|
|
3907
4161
|
if (!bytes) return null;
|
|
4162
|
+
if (!await shouldRenderImage(bytes, declared)) return null;
|
|
3908
4163
|
return rasterizeImage(bytes, {
|
|
3909
4164
|
maxCols,
|
|
3910
4165
|
maxRows: maxImageRows,
|
|
@@ -3922,24 +4177,25 @@ function createMarkdownView(opts) {
|
|
|
3922
4177
|
const imageNode = (image) => {
|
|
3923
4178
|
const label = `\u{1F5BC} ${image.alt.trim() || "image"}`;
|
|
3924
4179
|
const url = imageBase ? resolveReadmeImageUrl(image.href, imageBase) : null;
|
|
3925
|
-
if (!imagesEnabled || !url || !opts.loadImage)
|
|
4180
|
+
if (!imagesEnabled || !url || !opts.loadImage)
|
|
4181
|
+
return caption(label, image.linkUrl);
|
|
3926
4182
|
const container = new BoxRenderable3(renderer, {
|
|
3927
4183
|
flexDirection: "column",
|
|
3928
4184
|
flexShrink: 0,
|
|
3929
4185
|
width: "100%"
|
|
3930
4186
|
});
|
|
3931
|
-
const placeholder = caption(`${label} (loading\u2026)
|
|
4187
|
+
const placeholder = caption(`${label} (loading\u2026)`, image.linkUrl);
|
|
3932
4188
|
container.add(placeholder);
|
|
3933
|
-
void rasterize(url).then(
|
|
4189
|
+
void rasterize(url, image.declared).then(
|
|
3934
4190
|
(cells) => {
|
|
3935
4191
|
if (container.isDestroyed || !container.parent) return;
|
|
3936
4192
|
container.remove(placeholder);
|
|
3937
4193
|
placeholder.destroy();
|
|
3938
|
-
container.add(caption(label));
|
|
4194
|
+
container.add(caption(label, image.linkUrl));
|
|
3939
4195
|
if (cells) {
|
|
3940
4196
|
container.add(
|
|
3941
4197
|
new TextRenderable3(renderer, {
|
|
3942
|
-
content: imageStyledText(cells),
|
|
4198
|
+
content: imageStyledText(cells, image.linkUrl),
|
|
3943
4199
|
bg: bg(),
|
|
3944
4200
|
wrapMode: "none",
|
|
3945
4201
|
selectable: false,
|
|
@@ -3981,17 +4237,22 @@ function createMarkdownView(opts) {
|
|
|
3981
4237
|
const buildRenderNode = () => (token, _context) => {
|
|
3982
4238
|
if (token.type === "heading") return headingNode(token);
|
|
3983
4239
|
const images = extractImageRefs(token);
|
|
3984
|
-
if (images
|
|
3985
|
-
|
|
3986
|
-
|
|
4240
|
+
if (images === null) return void 0;
|
|
4241
|
+
if (images.length === 0) {
|
|
4242
|
+
return new BoxRenderable3(renderer, {
|
|
3987
4243
|
flexShrink: 0,
|
|
3988
4244
|
width: "100%",
|
|
3989
|
-
|
|
4245
|
+
height: 0
|
|
3990
4246
|
});
|
|
3991
|
-
for (const image of images) box.add(imageNode(image));
|
|
3992
|
-
return box;
|
|
3993
4247
|
}
|
|
3994
|
-
|
|
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;
|
|
3995
4256
|
};
|
|
3996
4257
|
renderable.renderNode = buildRenderNode();
|
|
3997
4258
|
return {
|
|
@@ -4017,29 +4278,6 @@ function createMarkdownView(opts) {
|
|
|
4017
4278
|
};
|
|
4018
4279
|
}
|
|
4019
4280
|
|
|
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
4281
|
// src/explore.ts
|
|
4044
4282
|
async function fetchTopics() {
|
|
4045
4283
|
const res = await fetch(
|
|
@@ -4189,23 +4427,23 @@ function nextTip() {
|
|
|
4189
4427
|
}
|
|
4190
4428
|
|
|
4191
4429
|
// src/update-check.ts
|
|
4192
|
-
import { readFileSync as
|
|
4193
|
-
import { join as
|
|
4430
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
4431
|
+
import { join as join6, dirname as dirname3, posix } from "path";
|
|
4194
4432
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4195
4433
|
var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
|
|
4196
4434
|
var PKG_NAME2 = "github-search-cli";
|
|
4197
4435
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
4198
4436
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
4199
4437
|
var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
|
|
4200
|
-
var PKG_DIR2 =
|
|
4438
|
+
var PKG_DIR2 = join6(__dirname2, "..");
|
|
4201
4439
|
var STATE_FILE = "update-state.json";
|
|
4202
|
-
var
|
|
4440
|
+
var DEFAULTS2 = {
|
|
4203
4441
|
lastCheck: 0,
|
|
4204
4442
|
snoozeUntil: 0,
|
|
4205
4443
|
suppressed: false
|
|
4206
4444
|
};
|
|
4207
4445
|
function readUpdateState() {
|
|
4208
|
-
return { ...
|
|
4446
|
+
return { ...DEFAULTS2, ...readJSON(STATE_FILE, {}) };
|
|
4209
4447
|
}
|
|
4210
4448
|
function writeUpdateState(state) {
|
|
4211
4449
|
writeJSON(STATE_FILE, state);
|
|
@@ -4338,14 +4576,14 @@ function recordPreUpdateState(currentVersion) {
|
|
|
4338
4576
|
}
|
|
4339
4577
|
function fetchReleaseNotes(version) {
|
|
4340
4578
|
try {
|
|
4341
|
-
const notesPath =
|
|
4342
|
-
const md =
|
|
4579
|
+
const notesPath = join6(PKG_DIR2, "release-notes", `${version}.md`);
|
|
4580
|
+
const md = readFileSync6(notesPath, "utf-8");
|
|
4343
4581
|
if (md.trim().length > 0) return md;
|
|
4344
4582
|
} catch {
|
|
4345
4583
|
}
|
|
4346
4584
|
try {
|
|
4347
|
-
const changelogPath =
|
|
4348
|
-
const changelog =
|
|
4585
|
+
const changelogPath = join6(PKG_DIR2, "CHANGELOG.md");
|
|
4586
|
+
const changelog = readFileSync6(changelogPath, "utf-8");
|
|
4349
4587
|
const header = `## [${version}]`;
|
|
4350
4588
|
const start = changelog.indexOf(header);
|
|
4351
4589
|
if (start === -1) return null;
|
|
@@ -4464,7 +4702,7 @@ async function launchBrowser(theme_override) {
|
|
|
4464
4702
|
const root = renderer.root;
|
|
4465
4703
|
root.flexDirection = "column";
|
|
4466
4704
|
const config = loadConfig();
|
|
4467
|
-
|
|
4705
|
+
let githubToken = config.githubToken || process.env.GITHUB_TOKEN;
|
|
4468
4706
|
const theme = loadTheme(theme_override || config.theme);
|
|
4469
4707
|
Object.assign(colors, theme);
|
|
4470
4708
|
if (terminalBg) {
|
|
@@ -4647,6 +4885,10 @@ async function launchBrowser(theme_override) {
|
|
|
4647
4885
|
currentMode = "search";
|
|
4648
4886
|
showLanding(false);
|
|
4649
4887
|
showSearchMode();
|
|
4888
|
+
if (currentQueryInput) {
|
|
4889
|
+
searchInput.value = currentQueryInput;
|
|
4890
|
+
renderer.requestRender();
|
|
4891
|
+
}
|
|
4650
4892
|
}
|
|
4651
4893
|
},
|
|
4652
4894
|
{
|
|
@@ -5942,7 +6184,8 @@ async function launchBrowser(theme_override) {
|
|
|
5942
6184
|
showOverlay("readme");
|
|
5943
6185
|
try {
|
|
5944
6186
|
const readme = await fetchReadme(repo.owner, repo.name, {
|
|
5945
|
-
token: githubToken
|
|
6187
|
+
token: githubToken,
|
|
6188
|
+
private: repo.private
|
|
5946
6189
|
});
|
|
5947
6190
|
if (readme) {
|
|
5948
6191
|
readmeView.setContent(readme.text, readme.sourceUrl);
|
|
@@ -5970,9 +6213,85 @@ async function launchBrowser(theme_override) {
|
|
|
5970
6213
|
setStatus(nextTip());
|
|
5971
6214
|
}
|
|
5972
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
|
+
}
|
|
5973
6286
|
function setStatus(msg) {
|
|
5974
6287
|
statusMgr.set("idle", msg);
|
|
5975
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
|
+
}
|
|
5976
6295
|
function buildSessionState() {
|
|
5977
6296
|
const mode = currentMode === "trending" ? "trending" : currentMode === "packages" ? "packages" : "search";
|
|
5978
6297
|
return {
|
|
@@ -6189,6 +6508,7 @@ async function launchBrowser(theme_override) {
|
|
|
6189
6508
|
async function loadTrending() {
|
|
6190
6509
|
currentMode = "trending";
|
|
6191
6510
|
isLoading = true;
|
|
6511
|
+
errorHint = null;
|
|
6192
6512
|
searchBox.visible = false;
|
|
6193
6513
|
toolbarText.visible = false;
|
|
6194
6514
|
trendingTabBox.visible = true;
|
|
@@ -6238,11 +6558,10 @@ async function launchBrowser(theme_override) {
|
|
|
6238
6558
|
resultCount: fetched.length
|
|
6239
6559
|
});
|
|
6240
6560
|
} catch (err) {
|
|
6241
|
-
const msg = err instanceof SearchCliError ? err.userMessage : err instanceof Error ? err.message : String(err);
|
|
6242
6561
|
resultsSelect.options = [
|
|
6243
6562
|
{ name: " (error)", description: "", value: null }
|
|
6244
6563
|
];
|
|
6245
|
-
|
|
6564
|
+
reportError2(err);
|
|
6246
6565
|
}
|
|
6247
6566
|
isLoading = false;
|
|
6248
6567
|
renderer.requestRender();
|
|
@@ -6281,6 +6600,7 @@ async function launchBrowser(theme_override) {
|
|
|
6281
6600
|
if (q === "" || isLoading) return;
|
|
6282
6601
|
currentQueryInput = q;
|
|
6283
6602
|
isLoading = true;
|
|
6603
|
+
errorHint = null;
|
|
6284
6604
|
resultsSelect.options = [
|
|
6285
6605
|
{ name: " Searching packages...", description: "", value: null }
|
|
6286
6606
|
];
|
|
@@ -6318,6 +6638,7 @@ async function launchBrowser(theme_override) {
|
|
|
6318
6638
|
async function doSearch(queryText, append = false) {
|
|
6319
6639
|
const q = queryText.trim();
|
|
6320
6640
|
if (q === "" || isLoading) return;
|
|
6641
|
+
errorHint = null;
|
|
6321
6642
|
if (!append) currentPage = 1;
|
|
6322
6643
|
deepDiveActive = false;
|
|
6323
6644
|
isLoading = true;
|
|
@@ -6374,12 +6695,13 @@ async function launchBrowser(theme_override) {
|
|
|
6374
6695
|
}
|
|
6375
6696
|
}
|
|
6376
6697
|
} catch (err) {
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6698
|
+
if (!append) {
|
|
6699
|
+
currentRepos = [];
|
|
6700
|
+
resultsSelect.options = [
|
|
6701
|
+
{ name: " (error)", description: "", value: null }
|
|
6702
|
+
];
|
|
6703
|
+
}
|
|
6704
|
+
reportError2(err);
|
|
6383
6705
|
}
|
|
6384
6706
|
isLoading = false;
|
|
6385
6707
|
renderer.requestRender();
|
|
@@ -6476,6 +6798,16 @@ ${pack.description ?? ""}`;
|
|
|
6476
6798
|
}
|
|
6477
6799
|
});
|
|
6478
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
|
+
}
|
|
6479
6811
|
if (currentOverlay !== "none") {
|
|
6480
6812
|
if (key.name === "escape") {
|
|
6481
6813
|
if (currentOverlay === "leader") {
|
|
@@ -6779,6 +7111,27 @@ ${pack.description ?? ""}`;
|
|
|
6779
7111
|
showLeaderMenu();
|
|
6780
7112
|
return;
|
|
6781
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
|
+
}
|
|
6782
7135
|
if (key.name === "t" && !searchInput.focused) {
|
|
6783
7136
|
const current = config.theme;
|
|
6784
7137
|
const themes = listThemes();
|
|
@@ -6873,38 +7226,18 @@ ${pack.description ?? ""}`;
|
|
|
6873
7226
|
}
|
|
6874
7227
|
});
|
|
6875
7228
|
const _currentVersion = getVersion();
|
|
6876
|
-
|
|
6877
|
-
showLanding(true);
|
|
6878
|
-
}
|
|
7229
|
+
showLanding(true);
|
|
6879
7230
|
checkForUpdateAndShow();
|
|
6880
7231
|
checkPostUpgradePanel();
|
|
6881
7232
|
renderer.start();
|
|
6882
|
-
if (session?.mode) {
|
|
6883
|
-
if (currentMode === "trending") {
|
|
6884
|
-
loadTrending();
|
|
6885
|
-
} else {
|
|
6886
|
-
searchInput.value = currentQueryInput;
|
|
6887
|
-
if (currentQueryInput) {
|
|
6888
|
-
doSearch(currentQueryInput);
|
|
6889
|
-
} else {
|
|
6890
|
-
showSearchMode();
|
|
6891
|
-
}
|
|
6892
|
-
searchInput.focus();
|
|
6893
|
-
}
|
|
6894
|
-
}
|
|
6895
7233
|
renderer.requestRender();
|
|
6896
7234
|
}
|
|
6897
7235
|
function formatResultLine(repo) {
|
|
6898
|
-
const stars =
|
|
7236
|
+
const stars = `\u2605 ${fmtStars(repo.stars)}`;
|
|
6899
7237
|
const lang = (repo.language ?? "?").padEnd(12).slice(0, 12);
|
|
6900
7238
|
const name = repo.fullName.padEnd(40).slice(0, 40);
|
|
6901
7239
|
return `${name} ${lang} ${stars}`;
|
|
6902
7240
|
}
|
|
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
7241
|
function formatToolbar(sort, limit, totalCount) {
|
|
6909
7242
|
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
6910
7243
|
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
@@ -7034,14 +7367,15 @@ function formatSkillCatalog() {
|
|
|
7034
7367
|
}
|
|
7035
7368
|
|
|
7036
7369
|
// src/skill-finder.ts
|
|
7037
|
-
import { readdirSync, readFileSync as
|
|
7038
|
-
import { homedir as
|
|
7039
|
-
import { delimiter, join as
|
|
7370
|
+
import { readdirSync, readFileSync as readFileSync7 } from "fs";
|
|
7371
|
+
import { homedir as homedir4 } from "os";
|
|
7372
|
+
import { delimiter, join as join7 } from "path";
|
|
7040
7373
|
var REGISTRY_SEARCH_ENDPOINT = "https://skills.sh/api/search";
|
|
7041
7374
|
var MIN_QUERY_LENGTH = 2;
|
|
7042
7375
|
var MAX_SKILL_RESULTS = 100;
|
|
7376
|
+
var SAFE_ID_RE = /^[A-Za-z0-9._/-]+$/;
|
|
7043
7377
|
function displayPath(path) {
|
|
7044
|
-
const home =
|
|
7378
|
+
const home = homedir4();
|
|
7045
7379
|
const normalized = path.replace(/\\/g, "/");
|
|
7046
7380
|
const homeNorm = home.replace(/\\/g, "/").replace(/\/$/, "");
|
|
7047
7381
|
if (normalized === homeNorm) return "~";
|
|
@@ -7051,12 +7385,12 @@ function displayPath(path) {
|
|
|
7051
7385
|
return normalized;
|
|
7052
7386
|
}
|
|
7053
7387
|
function defaultSkillRoots(cwd = process.cwd()) {
|
|
7054
|
-
const home =
|
|
7388
|
+
const home = homedir4();
|
|
7055
7389
|
return [
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7390
|
+
join7(home, ".codex", "skills"),
|
|
7391
|
+
join7(home, ".agents", "skills"),
|
|
7392
|
+
join7(cwd, ".codex", "skills"),
|
|
7393
|
+
join7(cwd, ".github", "skills")
|
|
7060
7394
|
];
|
|
7061
7395
|
}
|
|
7062
7396
|
function resolveSkillRoots(cwd = process.cwd()) {
|
|
@@ -7098,16 +7432,20 @@ function parseSkillFrontmatter(text) {
|
|
|
7098
7432
|
if (!name) return null;
|
|
7099
7433
|
return { name, description: fields.description ?? "" };
|
|
7100
7434
|
}
|
|
7435
|
+
var scanCache = /* @__PURE__ */ new Map();
|
|
7101
7436
|
function scanLocalSkills(roots = resolveSkillRoots()) {
|
|
7437
|
+
const key = roots.map((r) => `${r.path}\0${r.label}`).join("");
|
|
7438
|
+
const cached2 = scanCache.get(key);
|
|
7439
|
+
if (cached2) return cached2;
|
|
7102
7440
|
const byName = /* @__PURE__ */ new Map();
|
|
7103
7441
|
for (const root of roots) {
|
|
7104
7442
|
for (const dir of walkDirs(root.path)) {
|
|
7105
7443
|
const entries = listDir(dir);
|
|
7106
7444
|
if (!entries) continue;
|
|
7107
7445
|
if (!entries.some((e) => e.isFile() && e.name === "SKILL.md")) continue;
|
|
7108
|
-
const file =
|
|
7446
|
+
const file = join7(dir, "SKILL.md");
|
|
7109
7447
|
try {
|
|
7110
|
-
const parsed = parseSkillFrontmatter(
|
|
7448
|
+
const parsed = parseSkillFrontmatter(readFileSync7(file, "utf-8"));
|
|
7111
7449
|
if (!parsed || byName.has(parsed.name)) continue;
|
|
7112
7450
|
byName.set(parsed.name, {
|
|
7113
7451
|
name: parsed.name,
|
|
@@ -7119,7 +7457,9 @@ function scanLocalSkills(roots = resolveSkillRoots()) {
|
|
|
7119
7457
|
}
|
|
7120
7458
|
}
|
|
7121
7459
|
}
|
|
7122
|
-
|
|
7460
|
+
const hits = [...byName.values()];
|
|
7461
|
+
scanCache.set(key, hits);
|
|
7462
|
+
return hits;
|
|
7123
7463
|
}
|
|
7124
7464
|
function listDir(dir) {
|
|
7125
7465
|
try {
|
|
@@ -7138,7 +7478,7 @@ function* walkDirs(root) {
|
|
|
7138
7478
|
if (depth >= MAX_DEPTH) continue;
|
|
7139
7479
|
for (const entry of entries) {
|
|
7140
7480
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
7141
|
-
queue.push([
|
|
7481
|
+
queue.push([join7(dir, entry.name), depth + 1]);
|
|
7142
7482
|
}
|
|
7143
7483
|
}
|
|
7144
7484
|
}
|
|
@@ -7184,7 +7524,10 @@ function normalizeRegistrySkill(raw) {
|
|
|
7184
7524
|
const obj = raw;
|
|
7185
7525
|
const source = typeof obj.source === "string" ? obj.source : "";
|
|
7186
7526
|
const skillId = typeof obj.skillId === "string" ? obj.skillId : "";
|
|
7187
|
-
|
|
7527
|
+
const hasTraversal = (s) => s === ".." || s.includes("..");
|
|
7528
|
+
if (!SAFE_ID_RE.test(source) || !SAFE_ID_RE.test(skillId) || hasTraversal(source) || hasTraversal(skillId)) {
|
|
7529
|
+
return null;
|
|
7530
|
+
}
|
|
7188
7531
|
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name : skillId;
|
|
7189
7532
|
const installs = typeof obj.installs === "number" ? obj.installs : 0;
|
|
7190
7533
|
const id = typeof obj.id === "string" && obj.id ? obj.id : `${source}/${skillId}`;
|
|
@@ -7374,7 +7717,18 @@ var TOOLS = [
|
|
|
7374
7717
|
verbose: false,
|
|
7375
7718
|
token
|
|
7376
7719
|
};
|
|
7377
|
-
|
|
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
|
+
}
|
|
7378
7732
|
const repos = rankRepos(response.repos, sort);
|
|
7379
7733
|
try {
|
|
7380
7734
|
appendHistory({
|
|
@@ -7580,19 +7934,14 @@ var TOOLS = [
|
|
|
7580
7934
|
throw new McpToolError("repos must contain 2-5 repo fullNames.");
|
|
7581
7935
|
}
|
|
7582
7936
|
const token = resolveToken(state, args.token);
|
|
7583
|
-
const provider = createGitHubSearch(void 0, token ? [token] : []);
|
|
7584
7937
|
const found = [];
|
|
7585
7938
|
const missing = [];
|
|
7586
7939
|
for (const fullName of names) {
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
token
|
|
7593
|
-
});
|
|
7594
|
-
if (res.repos.length > 0) found.push(res.repos[0]);
|
|
7595
|
-
else missing.push(fullName);
|
|
7940
|
+
try {
|
|
7941
|
+
found.push(await resolveRepoFromRef(fullName, token));
|
|
7942
|
+
} catch {
|
|
7943
|
+
missing.push(fullName);
|
|
7944
|
+
}
|
|
7596
7945
|
}
|
|
7597
7946
|
if (found.length < 2) {
|
|
7598
7947
|
throw new McpToolError(
|
|
@@ -7895,7 +8244,16 @@ async function runMcpServer(options = {}) {
|
|
|
7895
8244
|
// src/cli.ts
|
|
7896
8245
|
process.env.GHFIND_CLI_RUN = "1";
|
|
7897
8246
|
var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
|
|
7898
|
-
var PKG_DIR3 =
|
|
8247
|
+
var PKG_DIR3 = join8(__dirname3, "..");
|
|
8248
|
+
var SORT_VALUES = [
|
|
8249
|
+
"best-match",
|
|
8250
|
+
"stars",
|
|
8251
|
+
"updated",
|
|
8252
|
+
"forks",
|
|
8253
|
+
"score",
|
|
8254
|
+
"downloads",
|
|
8255
|
+
"name"
|
|
8256
|
+
];
|
|
7899
8257
|
function parseArgs(args) {
|
|
7900
8258
|
const flags = {
|
|
7901
8259
|
query: "",
|
|
@@ -7994,9 +8352,17 @@ function parseArgs(args) {
|
|
|
7994
8352
|
}
|
|
7995
8353
|
break;
|
|
7996
8354
|
}
|
|
7997
|
-
case "--sort":
|
|
7998
|
-
|
|
8355
|
+
case "--sort": {
|
|
8356
|
+
const sort = args[++i];
|
|
8357
|
+
if (!sort || !SORT_VALUES.includes(sort)) {
|
|
8358
|
+
console.error(
|
|
8359
|
+
`Invalid sort: ${sort ?? "(missing)"} (use best-match|stars|updated|forks).`
|
|
8360
|
+
);
|
|
8361
|
+
process.exit(1);
|
|
8362
|
+
}
|
|
8363
|
+
flags.sort = sort;
|
|
7999
8364
|
break;
|
|
8365
|
+
}
|
|
8000
8366
|
case "--token":
|
|
8001
8367
|
flags.token = args[++i];
|
|
8002
8368
|
break;
|
|
@@ -8209,8 +8575,8 @@ Options:
|
|
|
8209
8575
|
if (flags.completion) {
|
|
8210
8576
|
const shell = flags.completion;
|
|
8211
8577
|
try {
|
|
8212
|
-
const content =
|
|
8213
|
-
|
|
8578
|
+
const content = readFileSync8(
|
|
8579
|
+
join8(PKG_DIR3, "completions", `ghfind.${shell}`),
|
|
8214
8580
|
"utf-8"
|
|
8215
8581
|
);
|
|
8216
8582
|
console.log(content);
|
|
@@ -8524,10 +8890,11 @@ function rowsToCsv(header, rows) {
|
|
|
8524
8890
|
return [header, ...rows].map((row) => row.map(quote).join(",")).join("\n");
|
|
8525
8891
|
}
|
|
8526
8892
|
function rowsToMarkdown(header, rows) {
|
|
8893
|
+
const cell = (value) => value.replace(/\|/g, "\\|");
|
|
8527
8894
|
return [
|
|
8528
|
-
`| ${header.join(" | ")} |`,
|
|
8895
|
+
`| ${header.map(cell).join(" | ")} |`,
|
|
8529
8896
|
`|${header.map(() => "---").join("|")}|`,
|
|
8530
|
-
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
8897
|
+
...rows.map((row) => `| ${row.map(cell).join(" | ")} |`)
|
|
8531
8898
|
].join("\n");
|
|
8532
8899
|
}
|
|
8533
8900
|
function truncate2(text, width) {
|
|
@@ -8567,7 +8934,12 @@ async function runReadme(flags) {
|
|
|
8567
8934
|
}
|
|
8568
8935
|
try {
|
|
8569
8936
|
const { owner, name } = repoFromRef(ref);
|
|
8570
|
-
const readme = await fetchReadme(owner, name, {
|
|
8937
|
+
const readme = await fetchReadme(owner, name, {
|
|
8938
|
+
// A bare ref carries no visibility info, so the token is never
|
|
8939
|
+
// forwarded to raw.githubusercontent.com.
|
|
8940
|
+
token: flags.token,
|
|
8941
|
+
private: false
|
|
8942
|
+
});
|
|
8571
8943
|
if (!readme) {
|
|
8572
8944
|
console.error(`No README found for ${owner}/${name}.`);
|
|
8573
8945
|
process.exit(1);
|
|
@@ -8816,22 +9188,14 @@ async function runNonInteractive(flags, outputFormat) {
|
|
|
8816
9188
|
}
|
|
8817
9189
|
},
|
|
8818
9190
|
compare: async (_ctx) => {
|
|
8819
|
-
const search = createGitHubSearch(void 0, [flags.token ?? ""]);
|
|
8820
9191
|
const repos = [];
|
|
8821
9192
|
const missing = [];
|
|
8822
9193
|
for (const fullName of flags.compare) {
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
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);
|
|
9194
|
+
try {
|
|
9195
|
+
repos.push(await resolveRepoFromRef(fullName, flags.token));
|
|
9196
|
+
} catch {
|
|
9197
|
+
missing.push(fullName);
|
|
9198
|
+
}
|
|
8835
9199
|
}
|
|
8836
9200
|
if (repos.length < 2) {
|
|
8837
9201
|
console.error(
|
|
@@ -8858,10 +9222,8 @@ Not found: ${missing.join(", ")}` : "";
|
|
|
8858
9222
|
await handlers[key](ctx);
|
|
8859
9223
|
}
|
|
8860
9224
|
main().catch((err) => {
|
|
8861
|
-
if (err instanceof
|
|
8862
|
-
console.error(
|
|
8863
|
-
err instanceof SearchCliError ? err.userMessage : err.message
|
|
8864
|
-
);
|
|
9225
|
+
if (err instanceof SearchCliError) {
|
|
9226
|
+
console.error(err.userMessage);
|
|
8865
9227
|
} else {
|
|
8866
9228
|
console.error(err instanceof Error ? err.message : String(err));
|
|
8867
9229
|
reportError(err);
|