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