github-search-cli 9.7.1 → 9.8.1
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 +80 -0
- package/README.md +79 -24
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +6 -1
- package/completions/ghfind.zsh +14 -1
- package/dist/cli.js +1506 -264
- package/package.json +7 -2
- package/release-notes/9.8.0.md +55 -0
- package/release-notes/9.8.1.md +74 -0
- package/src/agent-skill.ts +14 -0
- package/src/cli.ts +413 -1
- package/src/help.ts +2 -1
- package/src/image-loader.ts +277 -0
- package/src/image-render.ts +209 -0
- package/src/markdown-images.ts +123 -0
- package/src/markdown-view.ts +316 -0
- package/src/mcp-server.ts +60 -0
- package/src/png.ts +260 -0
- package/src/readme.ts +54 -0
- package/src/skill-finder.ts +445 -0
- package/src/tui.ts +72 -50
- package/src/markdown-render.ts +0 -211
package/dist/cli.js
CHANGED
|
@@ -1538,7 +1538,7 @@ var cached = null;
|
|
|
1538
1538
|
function getVersion() {
|
|
1539
1539
|
if (cached) return cached;
|
|
1540
1540
|
if (true) {
|
|
1541
|
-
cached = "9.
|
|
1541
|
+
cached = "9.8.1";
|
|
1542
1542
|
return cached;
|
|
1543
1543
|
}
|
|
1544
1544
|
try {
|
|
@@ -2309,6 +2309,12 @@ function removeBookmark(fullName) {
|
|
|
2309
2309
|
const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
|
|
2310
2310
|
saveRaw(bookmarks);
|
|
2311
2311
|
}
|
|
2312
|
+
function searchBookmarks(query) {
|
|
2313
|
+
const q = query.toLowerCase();
|
|
2314
|
+
return getBookmarks().filter(
|
|
2315
|
+
(b) => b.repo.fullName.toLowerCase().includes(q) || (b.repo.description ?? "").toLowerCase().includes(q) || b.tags.some((t3) => t3.toLowerCase().includes(q))
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2312
2318
|
function getLastSeen(fullName) {
|
|
2313
2319
|
return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
|
|
2314
2320
|
}
|
|
@@ -2382,8 +2388,8 @@ var PER_REPO = 5;
|
|
|
2382
2388
|
function loadCache() {
|
|
2383
2389
|
return readJSON(RELEASES_FILE, {});
|
|
2384
2390
|
}
|
|
2385
|
-
function saveCache(
|
|
2386
|
-
writeJSON(RELEASES_FILE,
|
|
2391
|
+
function saveCache(cache2) {
|
|
2392
|
+
writeJSON(RELEASES_FILE, cache2);
|
|
2387
2393
|
}
|
|
2388
2394
|
function toTracked(raw) {
|
|
2389
2395
|
return raw.filter((r) => r.draft !== true).map((r) => ({
|
|
@@ -2414,13 +2420,13 @@ async function fetchReleases(fullName, etag, token) {
|
|
|
2414
2420
|
}
|
|
2415
2421
|
async function checkReleases(opts = {}) {
|
|
2416
2422
|
const bookmarks = getBookmarks();
|
|
2417
|
-
const
|
|
2423
|
+
const cache2 = loadCache();
|
|
2418
2424
|
const config = loadConfig();
|
|
2419
2425
|
const token = opts.token ?? config.githubToken ?? process.env.GITHUB_TOKEN;
|
|
2420
2426
|
const results2 = [];
|
|
2421
2427
|
for (const bm of bookmarks) {
|
|
2422
2428
|
try {
|
|
2423
|
-
const prev =
|
|
2429
|
+
const prev = cache2[bm.repo.fullName];
|
|
2424
2430
|
const lastSeen = getLastSeen(bm.repo.fullName);
|
|
2425
2431
|
const isNew = (r) => !r.publishedAt || new Date(r.publishedAt).getTime() > lastSeen;
|
|
2426
2432
|
const { status, etag, releases } = await fetchReleases(
|
|
@@ -2443,7 +2449,7 @@ async function checkReleases(opts = {}) {
|
|
|
2443
2449
|
});
|
|
2444
2450
|
continue;
|
|
2445
2451
|
}
|
|
2446
|
-
|
|
2452
|
+
cache2[bm.repo.fullName] = { checkedAt: Date.now(), etag, releases };
|
|
2447
2453
|
results2.push({
|
|
2448
2454
|
fullName: bm.repo.fullName,
|
|
2449
2455
|
newReleases: releases.filter(isNew)
|
|
@@ -2456,7 +2462,7 @@ async function checkReleases(opts = {}) {
|
|
|
2456
2462
|
});
|
|
2457
2463
|
}
|
|
2458
2464
|
}
|
|
2459
|
-
saveCache(
|
|
2465
|
+
saveCache(cache2);
|
|
2460
2466
|
return results2;
|
|
2461
2467
|
}
|
|
2462
2468
|
async function checkAndNotify(opts = {}) {
|
|
@@ -2473,9 +2479,9 @@ async function checkAndNotify(opts = {}) {
|
|
|
2473
2479
|
return results2;
|
|
2474
2480
|
}
|
|
2475
2481
|
function allCachedReleases() {
|
|
2476
|
-
const
|
|
2482
|
+
const cache2 = loadCache();
|
|
2477
2483
|
const bookmarks = getBookmarks();
|
|
2478
|
-
return Object.entries(
|
|
2484
|
+
return Object.entries(cache2).filter(([fullName]) => bookmarks.some((b) => b.repo.fullName === fullName)).flatMap(([fullName, s]) => s.releases.map((r) => ({ ...r, fullName }))).sort(
|
|
2479
2485
|
(a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
|
|
2480
2486
|
);
|
|
2481
2487
|
}
|
|
@@ -2946,15 +2952,15 @@ function buildDeepDiveText(data) {
|
|
|
2946
2952
|
}
|
|
2947
2953
|
|
|
2948
2954
|
// src/cli.ts
|
|
2949
|
-
import { readFileSync as
|
|
2950
|
-
import { join as
|
|
2955
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2956
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
2951
2957
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2952
2958
|
|
|
2953
2959
|
// src/tui.ts
|
|
2954
2960
|
import {
|
|
2955
2961
|
createCliRenderer as createCliRenderer3,
|
|
2956
|
-
BoxRenderable as
|
|
2957
|
-
TextRenderable as
|
|
2962
|
+
BoxRenderable as BoxRenderable5,
|
|
2963
|
+
TextRenderable as TextRenderable5,
|
|
2958
2964
|
InputRenderable,
|
|
2959
2965
|
SelectRenderable,
|
|
2960
2966
|
ScrollBoxRenderable as ScrollBoxRenderable3
|
|
@@ -3059,7 +3065,7 @@ function buildHelpSections() {
|
|
|
3059
3065
|
{ keys: "Limit", action: "Cycle limit: 10 \u2192 25 \u2192 50 \u2192 100" },
|
|
3060
3066
|
{ keys: "Refresh", action: "Re-run current search" },
|
|
3061
3067
|
{ keys: "Graph", action: "Toggle commit activity chart" },
|
|
3062
|
-
{ keys: "Readme", action: "Full README viewer" },
|
|
3068
|
+
{ keys: "Readme", action: "Full markdown README viewer" },
|
|
3063
3069
|
{ keys: "Deep-dive", action: "Languages + contributors + excerpt" },
|
|
3064
3070
|
{ keys: "Bookmark", action: "Save / unsave selected repo" },
|
|
3065
3071
|
{ keys: "Compare", action: "Add to / remove from comparison" },
|
|
@@ -3099,6 +3105,7 @@ function buildHelpSections() {
|
|
|
3099
3105
|
{ keys: "d", action: "Delete entry (history, saved)" },
|
|
3100
3106
|
{ keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" },
|
|
3101
3107
|
{ keys: "\u2190 \u2192 / h l", action: "Navigate tabs / panels" },
|
|
3108
|
+
{ keys: "i", action: "Toggle inline images (README viewer)" },
|
|
3102
3109
|
{
|
|
3103
3110
|
keys: "PgUp/PgDn",
|
|
3104
3111
|
action: "Scroll update notes; Home/End jump to ends"
|
|
@@ -3260,170 +3267,777 @@ function restoreSession() {
|
|
|
3260
3267
|
return readJSON(SESSION_FILE, null);
|
|
3261
3268
|
}
|
|
3262
3269
|
|
|
3263
|
-
// src/markdown-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3270
|
+
// src/markdown-view.ts
|
|
3271
|
+
import {
|
|
3272
|
+
BoxRenderable as BoxRenderable3,
|
|
3273
|
+
MarkdownRenderable,
|
|
3274
|
+
RGBA,
|
|
3275
|
+
StyledText as StyledText3,
|
|
3276
|
+
SyntaxStyle,
|
|
3277
|
+
TextRenderable as TextRenderable3,
|
|
3278
|
+
createTextAttributes
|
|
3279
|
+
} from "@opentui/core";
|
|
3280
|
+
|
|
3281
|
+
// src/png.ts
|
|
3282
|
+
import { inflateSync } from "zlib";
|
|
3283
|
+
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
3284
|
+
function channelsForColorType(colorType) {
|
|
3285
|
+
switch (colorType) {
|
|
3286
|
+
case 0:
|
|
3287
|
+
return 1;
|
|
3288
|
+
// greyscale
|
|
3289
|
+
case 2:
|
|
3290
|
+
return 3;
|
|
3291
|
+
// truecolour
|
|
3292
|
+
case 3:
|
|
3293
|
+
return 1;
|
|
3294
|
+
// palette index
|
|
3295
|
+
case 4:
|
|
3296
|
+
return 2;
|
|
3297
|
+
// greyscale + alpha
|
|
3298
|
+
case 6:
|
|
3299
|
+
return 4;
|
|
3300
|
+
// truecolour + alpha
|
|
3301
|
+
default:
|
|
3302
|
+
throw new Error(`Unsupported PNG colour type ${colorType}`);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
function paeth(a, b, c) {
|
|
3306
|
+
const p = a + b - c;
|
|
3307
|
+
const pa = Math.abs(p - a);
|
|
3308
|
+
const pb = Math.abs(p - b);
|
|
3309
|
+
const pc = Math.abs(p - c);
|
|
3310
|
+
if (pa <= pb && pa <= pc) return a;
|
|
3311
|
+
if (pb <= pc) return b;
|
|
3312
|
+
return c;
|
|
3313
|
+
}
|
|
3314
|
+
function unfilter(raw, height, bytesPerRow, bytesPerPixel) {
|
|
3315
|
+
const out = new Uint8Array(height * bytesPerRow);
|
|
3316
|
+
let src = 0;
|
|
3317
|
+
for (let y = 0; y < height; y++) {
|
|
3318
|
+
const filter = raw[src++];
|
|
3319
|
+
const rowStart = y * bytesPerRow;
|
|
3320
|
+
const prevStart = rowStart - bytesPerRow;
|
|
3321
|
+
for (let x = 0; x < bytesPerRow; x++) {
|
|
3322
|
+
const value = raw[src + x];
|
|
3323
|
+
const left = x >= bytesPerPixel ? out[rowStart + x - bytesPerPixel] : 0;
|
|
3324
|
+
const up = y > 0 ? out[prevStart + x] : 0;
|
|
3325
|
+
const upLeft = y > 0 && x >= bytesPerPixel ? out[prevStart + x - bytesPerPixel] : 0;
|
|
3326
|
+
let result;
|
|
3327
|
+
switch (filter) {
|
|
3328
|
+
case 0:
|
|
3329
|
+
result = value;
|
|
3330
|
+
break;
|
|
3331
|
+
case 1:
|
|
3332
|
+
result = value + left;
|
|
3333
|
+
break;
|
|
3334
|
+
case 2:
|
|
3335
|
+
result = value + up;
|
|
3336
|
+
break;
|
|
3337
|
+
case 3:
|
|
3338
|
+
result = value + (left + up >> 1);
|
|
3339
|
+
break;
|
|
3340
|
+
case 4:
|
|
3341
|
+
result = value + paeth(left, up, upLeft);
|
|
3342
|
+
break;
|
|
3343
|
+
default:
|
|
3344
|
+
throw new Error(`Unsupported PNG filter type ${filter}`);
|
|
3345
|
+
}
|
|
3346
|
+
out[rowStart + x] = result & 255;
|
|
3347
|
+
}
|
|
3348
|
+
src += bytesPerRow;
|
|
3349
|
+
}
|
|
3350
|
+
return out;
|
|
3351
|
+
}
|
|
3352
|
+
function sampleAt(row, offsetInRow, index, bitDepth) {
|
|
3353
|
+
if (bitDepth === 8) return row[offsetInRow + index];
|
|
3354
|
+
if (bitDepth === 16) return row[offsetInRow + index * 2];
|
|
3355
|
+
const bitOffset = index * bitDepth;
|
|
3356
|
+
const byte = row[offsetInRow + (bitOffset >> 3)];
|
|
3357
|
+
const shift = 8 - bitDepth - (bitOffset & 7);
|
|
3358
|
+
return byte >> shift & (1 << bitDepth) - 1;
|
|
3359
|
+
}
|
|
3360
|
+
function decodePng(bytes) {
|
|
3361
|
+
if (bytes.length < 8) throw new Error("PNG too short");
|
|
3362
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) {
|
|
3363
|
+
if (bytes[i] !== PNG_SIGNATURE[i]) throw new Error("Not a PNG file");
|
|
3364
|
+
}
|
|
3365
|
+
let width = 0;
|
|
3366
|
+
let height = 0;
|
|
3367
|
+
let bitDepth = 8;
|
|
3368
|
+
let colorType = 6;
|
|
3369
|
+
let interlace = 0;
|
|
3370
|
+
let palette = null;
|
|
3371
|
+
let transparency = null;
|
|
3372
|
+
const idat = [];
|
|
3373
|
+
let offset = 8;
|
|
3374
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3375
|
+
while (offset + 8 <= bytes.length) {
|
|
3376
|
+
const length = view.getUint32(offset);
|
|
3377
|
+
const type = String.fromCharCode(
|
|
3378
|
+
bytes[offset + 4],
|
|
3379
|
+
bytes[offset + 5],
|
|
3380
|
+
bytes[offset + 6],
|
|
3381
|
+
bytes[offset + 7]
|
|
3291
3382
|
);
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3383
|
+
const dataStart = offset + 8;
|
|
3384
|
+
const dataEnd = dataStart + length;
|
|
3385
|
+
if (dataEnd > bytes.length) throw new Error("Truncated PNG chunk");
|
|
3386
|
+
switch (type) {
|
|
3387
|
+
case "IHDR": {
|
|
3388
|
+
width = view.getUint32(dataStart);
|
|
3389
|
+
height = view.getUint32(dataStart + 4);
|
|
3390
|
+
bitDepth = bytes[dataStart + 8];
|
|
3391
|
+
colorType = bytes[dataStart + 9];
|
|
3392
|
+
interlace = bytes[dataStart + 12];
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
case "PLTE":
|
|
3396
|
+
palette = bytes.subarray(dataStart, dataEnd);
|
|
3397
|
+
break;
|
|
3398
|
+
case "tRNS":
|
|
3399
|
+
transparency = bytes.subarray(dataStart, dataEnd);
|
|
3400
|
+
break;
|
|
3401
|
+
case "IDAT":
|
|
3402
|
+
idat.push(bytes.subarray(dataStart, dataEnd));
|
|
3403
|
+
break;
|
|
3404
|
+
default:
|
|
3405
|
+
break;
|
|
3300
3406
|
}
|
|
3301
|
-
|
|
3302
|
-
if (
|
|
3303
|
-
|
|
3304
|
-
|
|
3407
|
+
offset = dataEnd + 4;
|
|
3408
|
+
if (type === "IEND") break;
|
|
3409
|
+
}
|
|
3410
|
+
if (width === 0 || height === 0) throw new Error("PNG has no IHDR");
|
|
3411
|
+
if (interlace !== 0) throw new Error("Interlaced PNG is not supported");
|
|
3412
|
+
const channels = channelsForColorType(colorType);
|
|
3413
|
+
const bitsPerPixel = channels * bitDepth;
|
|
3414
|
+
const bytesPerRow = Math.ceil(width * bitsPerPixel / 8);
|
|
3415
|
+
const bytesPerPixel = Math.max(1, Math.ceil(bitsPerPixel / 8));
|
|
3416
|
+
const compressed = idat.length === 1 ? idat[0] : concat(idat);
|
|
3417
|
+
const raw = new Uint8Array(inflateSync(compressed));
|
|
3418
|
+
if (raw.length < height * (bytesPerRow + 1)) {
|
|
3419
|
+
throw new Error("Truncated PNG pixel data");
|
|
3420
|
+
}
|
|
3421
|
+
const rows = unfilter(raw, height, bytesPerRow, bytesPerPixel);
|
|
3422
|
+
const out = new Uint8Array(width * height * 4);
|
|
3423
|
+
const maxSample = (1 << Math.min(bitDepth, 8)) - 1;
|
|
3424
|
+
const scale = bitDepth === 16 ? 1 / 257 : 255 / maxSample;
|
|
3425
|
+
for (let y = 0; y < height; y++) {
|
|
3426
|
+
const row = rows.subarray(y * bytesPerRow, (y + 1) * bytesPerRow);
|
|
3427
|
+
for (let x = 0; x < width; x++) {
|
|
3428
|
+
const to8 = (sample) => bitDepth === 8 || bitDepth === 16 ? sample : Math.round(sample * scale);
|
|
3429
|
+
let r;
|
|
3430
|
+
let g;
|
|
3431
|
+
let b;
|
|
3432
|
+
let a = 255;
|
|
3433
|
+
switch (colorType) {
|
|
3434
|
+
case 0: {
|
|
3435
|
+
const grey = to8(sampleAt(row, 0, x, bitDepth));
|
|
3436
|
+
r = g = b = grey;
|
|
3437
|
+
break;
|
|
3438
|
+
}
|
|
3439
|
+
case 2:
|
|
3440
|
+
r = to8(sampleAt(row, 0, x * 3, bitDepth));
|
|
3441
|
+
g = to8(sampleAt(row, 0, x * 3 + 1, bitDepth));
|
|
3442
|
+
b = to8(sampleAt(row, 0, x * 3 + 2, bitDepth));
|
|
3443
|
+
break;
|
|
3444
|
+
case 3: {
|
|
3445
|
+
const index = sampleAt(row, 0, x, bitDepth);
|
|
3446
|
+
const p = index * 3;
|
|
3447
|
+
if (!palette || p + 2 >= palette.length)
|
|
3448
|
+
throw new Error("PNG palette entry missing");
|
|
3449
|
+
r = palette[p];
|
|
3450
|
+
g = palette[p + 1];
|
|
3451
|
+
b = palette[p + 2];
|
|
3452
|
+
if (transparency && index < transparency.length)
|
|
3453
|
+
a = transparency[index];
|
|
3454
|
+
break;
|
|
3455
|
+
}
|
|
3456
|
+
case 4: {
|
|
3457
|
+
const grey = to8(sampleAt(row, 0, x * 2, bitDepth));
|
|
3458
|
+
r = g = b = grey;
|
|
3459
|
+
a = to8(sampleAt(row, 0, x * 2 + 1, bitDepth));
|
|
3460
|
+
break;
|
|
3461
|
+
}
|
|
3462
|
+
default: {
|
|
3463
|
+
r = to8(sampleAt(row, 0, x * 4, bitDepth));
|
|
3464
|
+
g = to8(sampleAt(row, 0, x * 4 + 1, bitDepth));
|
|
3465
|
+
b = to8(sampleAt(row, 0, x * 4 + 2, bitDepth));
|
|
3466
|
+
a = to8(sampleAt(row, 0, x * 4 + 3, bitDepth));
|
|
3467
|
+
break;
|
|
3468
|
+
}
|
|
3305
3469
|
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
out.push(sep);
|
|
3311
|
-
dataRows.forEach((row, i) => {
|
|
3312
|
-
const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
|
|
3313
|
-
out.push(`\u2502 ${cells.map((c) => ` ${c} `).join("\u2502")}\u2502`);
|
|
3314
|
-
if (i === 0) out.push(mid);
|
|
3315
|
-
});
|
|
3316
|
-
out.push(end);
|
|
3317
|
-
}
|
|
3318
|
-
out.push("");
|
|
3319
|
-
tableRows = [];
|
|
3320
|
-
}
|
|
3321
|
-
function renderInline(line) {
|
|
3322
|
-
line = line.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
3323
|
-
line = line.replace(/\*([^*]+?)\*/g, "$1");
|
|
3324
|
-
line = line.replace(/`([^`]+)`/g, "\u27E8$1\u27E9");
|
|
3325
|
-
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
3326
|
-
line = line.replace(/!\[([^\]]*)\]\([^)]+\)/g, "\u{1F5BC} $1");
|
|
3327
|
-
return line;
|
|
3328
|
-
}
|
|
3329
|
-
function wrapLine(line, maxW) {
|
|
3330
|
-
if (line.length <= maxW) return [line];
|
|
3331
|
-
const parts = [];
|
|
3332
|
-
let remaining = line;
|
|
3333
|
-
while (remaining.length > maxW) {
|
|
3334
|
-
let breakAt = maxW;
|
|
3335
|
-
const spaceIdx = remaining.lastIndexOf(" ", maxW);
|
|
3336
|
-
if (spaceIdx > maxW / 2) breakAt = spaceIdx;
|
|
3337
|
-
parts.push(remaining.slice(0, breakAt));
|
|
3338
|
-
remaining = remaining.slice(breakAt).replace(/^ /, "");
|
|
3339
|
-
}
|
|
3340
|
-
if (remaining) parts.push(remaining);
|
|
3341
|
-
return parts;
|
|
3342
|
-
}
|
|
3343
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3344
|
-
const line = lines[i];
|
|
3345
|
-
if (/^```/.test(line)) {
|
|
3346
|
-
if (inCodeBlock) {
|
|
3347
|
-
flushCode();
|
|
3348
|
-
inCodeBlock = false;
|
|
3349
|
-
} else {
|
|
3350
|
-
inCodeBlock = true;
|
|
3351
|
-
codeLang = line.slice(3).trim();
|
|
3470
|
+
if (colorType === 0 && transparency && transparency.length >= 2) {
|
|
3471
|
+
const key = transparency[0] << 8 | transparency[1];
|
|
3472
|
+
const sample = sampleAt(row, 0, x, bitDepth);
|
|
3473
|
+
if (sample === key) a = 0;
|
|
3352
3474
|
}
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3475
|
+
const o = (y * width + x) * 4;
|
|
3476
|
+
out[o] = r;
|
|
3477
|
+
out[o + 1] = g;
|
|
3478
|
+
out[o + 2] = b;
|
|
3479
|
+
out[o + 3] = a;
|
|
3358
3480
|
}
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3481
|
+
}
|
|
3482
|
+
return { width, height, data: out };
|
|
3483
|
+
}
|
|
3484
|
+
function concat(chunks) {
|
|
3485
|
+
let total = 0;
|
|
3486
|
+
for (const c of chunks) total += c.length;
|
|
3487
|
+
const merged = new Uint8Array(total);
|
|
3488
|
+
let offset = 0;
|
|
3489
|
+
for (const c of chunks) {
|
|
3490
|
+
merged.set(c, offset);
|
|
3491
|
+
offset += c.length;
|
|
3492
|
+
}
|
|
3493
|
+
return merged;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
// src/image-render.ts
|
|
3497
|
+
var DEFAULT_CELL_ASPECT = 0.5;
|
|
3498
|
+
var HALF_BLOCK = "\u2580";
|
|
3499
|
+
function fitImageToCells(imgWidth, imgHeight, opts) {
|
|
3500
|
+
const cellAspect = opts.cellAspect ?? DEFAULT_CELL_ASPECT;
|
|
3501
|
+
const minCols = Math.max(1, Math.min(opts.maxCols, 8));
|
|
3502
|
+
if (imgWidth <= 0 || imgHeight <= 0) {
|
|
3503
|
+
return { cols: minCols, rows: 1 };
|
|
3504
|
+
}
|
|
3505
|
+
const rowsPerCol = cellAspect * (imgHeight / imgWidth);
|
|
3506
|
+
let cols = Math.max(minCols, Math.floor(opts.maxCols));
|
|
3507
|
+
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;
|
|
3364
3551
|
continue;
|
|
3365
3552
|
}
|
|
3366
|
-
|
|
3367
|
-
flushTable();
|
|
3368
|
-
inTable = false;
|
|
3553
|
+
runs.push({ text: HALF_BLOCK, top: cell.top, bottom: cell.bottom });
|
|
3369
3554
|
}
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
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);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
function readmeBaseUrl(owner, repo, branch, path = "README.md") {
|
|
3738
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
// src/markdown-images.ts
|
|
3742
|
+
var IMG_TAG = /<img\b[^>]*?src\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
3743
|
+
var ALT_ATTR = /\balt\s*=\s*["']([^"']*)["']/i;
|
|
3744
|
+
var HTML_TAG = /<\/?[a-z][^>]*>/gi;
|
|
3745
|
+
function imagesFromHtml(raw) {
|
|
3746
|
+
if (!raw || !/<img\b/i.test(raw)) return [];
|
|
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;
|
|
3756
|
+
}
|
|
3757
|
+
function inlinePlainText(tokens) {
|
|
3758
|
+
if (!tokens) return "";
|
|
3759
|
+
let out = "";
|
|
3760
|
+
for (const token of tokens) {
|
|
3761
|
+
if (token.type === "image") continue;
|
|
3762
|
+
if (token.type === "html") {
|
|
3763
|
+
out += (token.raw ?? "").replace(HTML_TAG, "");
|
|
3373
3764
|
continue;
|
|
3374
3765
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
const level = headingMatch[1].length;
|
|
3378
|
-
const text = renderInline(headingMatch[2]);
|
|
3379
|
-
const prefix = "\u2588 ".repeat(level);
|
|
3380
|
-
out.push("");
|
|
3381
|
-
out.push(prefix + text);
|
|
3382
|
-
out.push("");
|
|
3766
|
+
if (token.type === "br") {
|
|
3767
|
+
out += "\n";
|
|
3383
3768
|
continue;
|
|
3384
3769
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
const inner = renderInline(bqMatch[1]);
|
|
3388
|
-
for (const wl of wrapLine(inner, width - 4)) {
|
|
3389
|
-
out.push(`\u2502 ${wl}`);
|
|
3390
|
-
}
|
|
3770
|
+
if (token.text && (!token.tokens || token.tokens.length === 0)) {
|
|
3771
|
+
out += token.text;
|
|
3391
3772
|
continue;
|
|
3392
3773
|
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
const indent = ulMatch[1].length;
|
|
3396
|
-
const depth = Math.floor(indent / 2);
|
|
3397
|
-
const prefix = `${" ".repeat(depth)}\u2022 `;
|
|
3398
|
-
const inner = renderInline(ulMatch[2]);
|
|
3399
|
-
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
3400
|
-
out.push(prefix + wl);
|
|
3401
|
-
}
|
|
3774
|
+
if (token.tokens?.length) {
|
|
3775
|
+
out += inlinePlainText(token.tokens);
|
|
3402
3776
|
continue;
|
|
3403
3777
|
}
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3778
|
+
if (token.raw) out += token.raw.replace(HTML_TAG, "");
|
|
3779
|
+
}
|
|
3780
|
+
return out;
|
|
3781
|
+
}
|
|
3782
|
+
function extractImageRefs(token) {
|
|
3783
|
+
const images = [];
|
|
3784
|
+
const collect = (inline2) => {
|
|
3785
|
+
if (inline2.type === "image") {
|
|
3786
|
+
if (inline2.href)
|
|
3787
|
+
images.push({ href: inline2.href, alt: inline2.text ?? "" });
|
|
3788
|
+
return true;
|
|
3789
|
+
}
|
|
3790
|
+
if (inline2.type === "html") {
|
|
3791
|
+
const refs = imagesFromHtml(inline2.raw);
|
|
3792
|
+
if (refs.length > 0) {
|
|
3793
|
+
images.push(...refs);
|
|
3794
|
+
return true;
|
|
3412
3795
|
}
|
|
3413
|
-
|
|
3796
|
+
return !(inline2.raw ?? "").replace(HTML_TAG, "").trim();
|
|
3414
3797
|
}
|
|
3415
|
-
if (
|
|
3416
|
-
|
|
3417
|
-
|
|
3798
|
+
if (inline2.type === "text" || inline2.type === "escape") {
|
|
3799
|
+
return !(inline2.text ?? "").trim();
|
|
3800
|
+
}
|
|
3801
|
+
return false;
|
|
3802
|
+
};
|
|
3803
|
+
if (token.type === "image" && token.href) {
|
|
3804
|
+
return [{ href: token.href, alt: token.text ?? "" }];
|
|
3805
|
+
}
|
|
3806
|
+
if (token.type === "html") {
|
|
3807
|
+
const refs = imagesFromHtml(token.raw);
|
|
3808
|
+
return refs.length > 0 ? refs : null;
|
|
3809
|
+
}
|
|
3810
|
+
const inline = token.tokens ?? [];
|
|
3811
|
+
if (inline.length === 0) return null;
|
|
3812
|
+
for (const child of inline) {
|
|
3813
|
+
if (!collect(child)) return null;
|
|
3814
|
+
}
|
|
3815
|
+
return images.length > 0 ? images : null;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
// src/markdown-view.ts
|
|
3819
|
+
var MAX_IMAGE_ROWS = 24;
|
|
3820
|
+
var RASTER_CACHE_LIMIT = 32;
|
|
3821
|
+
function createMarkdownStyle(colors2) {
|
|
3822
|
+
const surface = colors2.surface ?? colors2.bg;
|
|
3823
|
+
return SyntaxStyle.fromStyles({
|
|
3824
|
+
default: { fg: colors2.text ?? "#ffffff" },
|
|
3825
|
+
conceal: { fg: colors2.muted ?? "#888888" },
|
|
3826
|
+
"markup.heading": { fg: colors2.accent ?? colors2.blue, bold: true },
|
|
3827
|
+
"markup.strong": { fg: colors2.text ?? "#ffffff", bold: true },
|
|
3828
|
+
"markup.italic": { fg: colors2.text ?? "#ffffff", italic: true },
|
|
3829
|
+
"markup.strikethrough": { fg: colors2.muted ?? "#888888", dim: true },
|
|
3830
|
+
"markup.raw": { fg: colors2.green ?? "#a6e3a1", bg: surface },
|
|
3831
|
+
"markup.raw.block": { fg: colors2.green ?? "#a6e3a1" },
|
|
3832
|
+
"markup.link": { fg: colors2.accent ?? colors2.blue },
|
|
3833
|
+
"markup.link.label": {
|
|
3834
|
+
fg: colors2.accent ?? colors2.blue,
|
|
3835
|
+
underline: true
|
|
3836
|
+
},
|
|
3837
|
+
"markup.link.url": { fg: colors2.muted ?? "#888888", underline: true },
|
|
3838
|
+
"markup.quote": { fg: colors2.muted ?? "#888888", italic: true },
|
|
3839
|
+
"markup.list": { fg: colors2.blue ?? colors2.accent }
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
function hexToRgb(hex) {
|
|
3843
|
+
const fallback = [0, 0, 0];
|
|
3844
|
+
if (!hex) return fallback;
|
|
3845
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
3846
|
+
if (!match) return fallback;
|
|
3847
|
+
const value = Number.parseInt(match[1], 16);
|
|
3848
|
+
return [value >> 16 & 255, value >> 8 & 255, value & 255];
|
|
3849
|
+
}
|
|
3850
|
+
function rgba(color) {
|
|
3851
|
+
return RGBA.fromValues(color[0], color[1], color[2], 255);
|
|
3852
|
+
}
|
|
3853
|
+
function imageStyledText(image) {
|
|
3854
|
+
const chunks = [];
|
|
3855
|
+
toImageRuns(image).forEach((runs, rowIndex) => {
|
|
3856
|
+
if (rowIndex > 0) chunks.push({ __isChunk: true, text: "\n" });
|
|
3857
|
+
for (const run2 of runs) {
|
|
3858
|
+
chunks.push({
|
|
3859
|
+
__isChunk: true,
|
|
3860
|
+
text: run2.text,
|
|
3861
|
+
fg: rgba(run2.top),
|
|
3862
|
+
bg: rgba(run2.bottom)
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
});
|
|
3866
|
+
return new StyledText3(chunks);
|
|
3867
|
+
}
|
|
3868
|
+
function createMarkdownView(opts) {
|
|
3869
|
+
const { renderer, colors: colors2 } = opts;
|
|
3870
|
+
const maxImageRows = opts.maxImageRows ?? MAX_IMAGE_ROWS;
|
|
3871
|
+
const rasterCache = /* @__PURE__ */ new Map();
|
|
3872
|
+
let imagesEnabled = opts.images ?? true;
|
|
3873
|
+
let imageBase = opts.imageBase;
|
|
3874
|
+
const text = () => colors2.text ?? "#ffffff";
|
|
3875
|
+
const bg = () => opts.background ?? colors2.bg ?? "#000000";
|
|
3876
|
+
const muted = () => colors2.muted ?? "#888888";
|
|
3877
|
+
const renderable = new MarkdownRenderable(renderer, {
|
|
3878
|
+
content: "",
|
|
3879
|
+
syntaxStyle: createMarkdownStyle(colors2),
|
|
3880
|
+
fg: text(),
|
|
3881
|
+
bg: bg(),
|
|
3882
|
+
conceal: true,
|
|
3883
|
+
streaming: true,
|
|
3884
|
+
internalBlockMode: "top-level",
|
|
3885
|
+
// Borderless columns are the top-level default; boxed tables read better
|
|
3886
|
+
// for the multi-column tables READMEs use.
|
|
3887
|
+
tableOptions: { style: "grid" },
|
|
3888
|
+
width: "100%"
|
|
3889
|
+
});
|
|
3890
|
+
const caption = (label, color = muted()) => new TextRenderable3(renderer, {
|
|
3891
|
+
content: ` ${label}`,
|
|
3892
|
+
fg: color,
|
|
3893
|
+
bg: bg(),
|
|
3894
|
+
wrapMode: "word",
|
|
3895
|
+
flexShrink: 0,
|
|
3896
|
+
width: "100%"
|
|
3897
|
+
});
|
|
3898
|
+
const rasterize = (url) => {
|
|
3899
|
+
const maxCols = Math.max(8, Math.floor(opts.getImageWidth?.() ?? 100));
|
|
3900
|
+
const key = `${maxCols}|${url}`;
|
|
3901
|
+
const cached2 = rasterCache.get(key);
|
|
3902
|
+
if (cached2) return cached2;
|
|
3903
|
+
const pending = (async () => {
|
|
3904
|
+
const loader = opts.loadImage;
|
|
3905
|
+
if (!loader) return null;
|
|
3906
|
+
const bytes = await loader(url);
|
|
3907
|
+
if (!bytes) return null;
|
|
3908
|
+
return rasterizeImage(bytes, {
|
|
3909
|
+
maxCols,
|
|
3910
|
+
maxRows: maxImageRows,
|
|
3911
|
+
background: hexToRgb(bg())
|
|
3912
|
+
});
|
|
3913
|
+
})();
|
|
3914
|
+
rasterCache.set(key, pending);
|
|
3915
|
+
while (rasterCache.size > RASTER_CACHE_LIMIT) {
|
|
3916
|
+
const oldest = rasterCache.keys().next().value;
|
|
3917
|
+
if (oldest === void 0) break;
|
|
3918
|
+
rasterCache.delete(oldest);
|
|
3919
|
+
}
|
|
3920
|
+
return pending;
|
|
3921
|
+
};
|
|
3922
|
+
const imageNode = (image) => {
|
|
3923
|
+
const label = `\u{1F5BC} ${image.alt.trim() || "image"}`;
|
|
3924
|
+
const url = imageBase ? resolveReadmeImageUrl(image.href, imageBase) : null;
|
|
3925
|
+
if (!imagesEnabled || !url || !opts.loadImage) return caption(label);
|
|
3926
|
+
const container = new BoxRenderable3(renderer, {
|
|
3927
|
+
flexDirection: "column",
|
|
3928
|
+
flexShrink: 0,
|
|
3929
|
+
width: "100%"
|
|
3930
|
+
});
|
|
3931
|
+
const placeholder = caption(`${label} (loading\u2026)`);
|
|
3932
|
+
container.add(placeholder);
|
|
3933
|
+
void rasterize(url).then(
|
|
3934
|
+
(cells) => {
|
|
3935
|
+
if (container.isDestroyed || !container.parent) return;
|
|
3936
|
+
container.remove(placeholder);
|
|
3937
|
+
placeholder.destroy();
|
|
3938
|
+
container.add(caption(label));
|
|
3939
|
+
if (cells) {
|
|
3940
|
+
container.add(
|
|
3941
|
+
new TextRenderable3(renderer, {
|
|
3942
|
+
content: imageStyledText(cells),
|
|
3943
|
+
bg: bg(),
|
|
3944
|
+
wrapMode: "none",
|
|
3945
|
+
selectable: false,
|
|
3946
|
+
flexShrink: 0
|
|
3947
|
+
})
|
|
3948
|
+
);
|
|
3949
|
+
}
|
|
3950
|
+
renderable.requestRender();
|
|
3951
|
+
},
|
|
3952
|
+
(err) => {
|
|
3953
|
+
void err;
|
|
3954
|
+
}
|
|
3955
|
+
);
|
|
3956
|
+
return container;
|
|
3957
|
+
};
|
|
3958
|
+
const headingNode = (token) => {
|
|
3959
|
+
const heading = inlinePlainText(token.tokens).trim();
|
|
3960
|
+
if (!heading) return void 0;
|
|
3961
|
+
const depth = token.depth ?? 1;
|
|
3962
|
+
const color = depth <= 2 ? colors2.accent : colors2.teal ?? colors2.accent;
|
|
3963
|
+
return new TextRenderable3(renderer, {
|
|
3964
|
+
content: new StyledText3([
|
|
3965
|
+
{
|
|
3966
|
+
__isChunk: true,
|
|
3967
|
+
text: heading,
|
|
3968
|
+
fg: RGBA.fromHex(color ?? "#ffffff"),
|
|
3969
|
+
attributes: createTextAttributes({
|
|
3970
|
+
bold: true,
|
|
3971
|
+
underline: depth === 1
|
|
3972
|
+
})
|
|
3973
|
+
}
|
|
3974
|
+
]),
|
|
3975
|
+
bg: bg(),
|
|
3976
|
+
wrapMode: "word",
|
|
3977
|
+
flexShrink: 0,
|
|
3978
|
+
width: "100%"
|
|
3979
|
+
});
|
|
3980
|
+
};
|
|
3981
|
+
const buildRenderNode = () => (token, _context) => {
|
|
3982
|
+
if (token.type === "heading") return headingNode(token);
|
|
3983
|
+
const images = extractImageRefs(token);
|
|
3984
|
+
if (images && images.length > 0) {
|
|
3985
|
+
const box = new BoxRenderable3(renderer, {
|
|
3986
|
+
flexDirection: "column",
|
|
3987
|
+
flexShrink: 0,
|
|
3988
|
+
width: "100%",
|
|
3989
|
+
marginTop: 1
|
|
3990
|
+
});
|
|
3991
|
+
for (const image of images) box.add(imageNode(image));
|
|
3992
|
+
return box;
|
|
3418
3993
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3994
|
+
return void 0;
|
|
3995
|
+
};
|
|
3996
|
+
renderable.renderNode = buildRenderNode();
|
|
3997
|
+
return {
|
|
3998
|
+
renderable,
|
|
3999
|
+
get imagesEnabled() {
|
|
4000
|
+
return imagesEnabled;
|
|
4001
|
+
},
|
|
4002
|
+
setContent(markdown, base) {
|
|
4003
|
+
if (base !== void 0) imageBase = base;
|
|
4004
|
+
renderable.syntaxStyle = createMarkdownStyle(colors2);
|
|
4005
|
+
renderable.fg = text();
|
|
4006
|
+
renderable.bg = bg();
|
|
4007
|
+
renderable.content = markdown;
|
|
4008
|
+
renderable.streaming = false;
|
|
4009
|
+
renderable.requestRender();
|
|
4010
|
+
},
|
|
4011
|
+
setImagesEnabled(enabled) {
|
|
4012
|
+
if (imagesEnabled === enabled) return;
|
|
4013
|
+
imagesEnabled = enabled;
|
|
4014
|
+
renderable.renderNode = buildRenderNode();
|
|
4015
|
+
renderable.requestRender();
|
|
3422
4016
|
}
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
|
|
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;
|
|
3423
4040
|
}
|
|
3424
|
-
if (inCodeBlock) flushCode();
|
|
3425
|
-
if (inTable) flushTable();
|
|
3426
|
-
return out.join("\n");
|
|
3427
4041
|
}
|
|
3428
4042
|
|
|
3429
4043
|
// src/explore.ts
|
|
@@ -3746,7 +4360,7 @@ function fetchReleaseNotes(version) {
|
|
|
3746
4360
|
}
|
|
3747
4361
|
|
|
3748
4362
|
// src/landing.ts
|
|
3749
|
-
import { BoxRenderable as
|
|
4363
|
+
import { BoxRenderable as BoxRenderable4, TextRenderable as TextRenderable4 } from "@opentui/core";
|
|
3750
4364
|
function landingKeysActive(mode, overlay) {
|
|
3751
4365
|
return mode === "landing" && overlay === "none";
|
|
3752
4366
|
}
|
|
@@ -3878,13 +4492,13 @@ async function launchBrowser(theme_override) {
|
|
|
3878
4492
|
let deepDiveActive = false;
|
|
3879
4493
|
const compareList = [];
|
|
3880
4494
|
const _quitArmed = false;
|
|
3881
|
-
const header = new
|
|
4495
|
+
const header = new TextRenderable5(renderer, {
|
|
3882
4496
|
content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
|
|
3883
4497
|
bg: colors.bg,
|
|
3884
4498
|
fg: colors.muted,
|
|
3885
4499
|
height: 1
|
|
3886
4500
|
});
|
|
3887
|
-
const trendingTabBox = new
|
|
4501
|
+
const trendingTabBox = new BoxRenderable5(renderer, {
|
|
3888
4502
|
flexDirection: "row",
|
|
3889
4503
|
height: 1,
|
|
3890
4504
|
backgroundColor: colors.surface,
|
|
@@ -3899,7 +4513,7 @@ async function launchBrowser(theme_override) {
|
|
|
3899
4513
|
TAB_NAMES.forEach((name, i) => {
|
|
3900
4514
|
const isActive = name === trendingTab;
|
|
3901
4515
|
const label = ` ${name} `;
|
|
3902
|
-
const tt = new
|
|
4516
|
+
const tt = new TextRenderable5(renderer, {
|
|
3903
4517
|
content: label,
|
|
3904
4518
|
fg: isActive ? colors.bg : colors.muted,
|
|
3905
4519
|
bg: isActive ? colors.blue : colors.bg,
|
|
@@ -3908,7 +4522,7 @@ async function launchBrowser(theme_override) {
|
|
|
3908
4522
|
trendingTabTexts.push(tt);
|
|
3909
4523
|
trendingTabBox.add(tt);
|
|
3910
4524
|
if (i < TAB_NAMES.length - 1) {
|
|
3911
|
-
const sp = new
|
|
4525
|
+
const sp = new TextRenderable5(renderer, {
|
|
3912
4526
|
content: " ",
|
|
3913
4527
|
fg: colors.muted,
|
|
3914
4528
|
bg: colors.bg,
|
|
@@ -3921,7 +4535,7 @@ async function launchBrowser(theme_override) {
|
|
|
3921
4535
|
}
|
|
3922
4536
|
root.add(header);
|
|
3923
4537
|
root.add(trendingTabBox);
|
|
3924
|
-
const searchBox = new
|
|
4538
|
+
const searchBox = new BoxRenderable5(renderer, {
|
|
3925
4539
|
visible: true,
|
|
3926
4540
|
width: "100%",
|
|
3927
4541
|
flexDirection: "row",
|
|
@@ -3940,7 +4554,7 @@ async function launchBrowser(theme_override) {
|
|
|
3940
4554
|
});
|
|
3941
4555
|
searchBox.add(searchInput);
|
|
3942
4556
|
root.add(searchBox);
|
|
3943
|
-
const toolbarText = new
|
|
4557
|
+
const toolbarText = new TextRenderable5(renderer, {
|
|
3944
4558
|
content: formatToolbar(currentSort, currentLimit, totalCount),
|
|
3945
4559
|
visible: true,
|
|
3946
4560
|
// hidden in trending mode
|
|
@@ -3949,14 +4563,14 @@ async function launchBrowser(theme_override) {
|
|
|
3949
4563
|
paddingX: 1
|
|
3950
4564
|
});
|
|
3951
4565
|
root.add(toolbarText);
|
|
3952
|
-
const body = new
|
|
4566
|
+
const body = new BoxRenderable5(renderer, {
|
|
3953
4567
|
flexDirection: "row",
|
|
3954
4568
|
flexGrow: 1,
|
|
3955
4569
|
gap: 1,
|
|
3956
4570
|
paddingY: 1,
|
|
3957
4571
|
paddingX: 1
|
|
3958
4572
|
});
|
|
3959
|
-
const resultsBox = new
|
|
4573
|
+
const resultsBox = new BoxRenderable5(renderer, {
|
|
3960
4574
|
border: true,
|
|
3961
4575
|
borderColor: colors.border,
|
|
3962
4576
|
title: " Results ",
|
|
@@ -3985,7 +4599,7 @@ async function launchBrowser(theme_override) {
|
|
|
3985
4599
|
itemSpacing: 0
|
|
3986
4600
|
});
|
|
3987
4601
|
resultsBox.add(resultsSelect);
|
|
3988
|
-
const detailBox = new
|
|
4602
|
+
const detailBox = new BoxRenderable5(renderer, {
|
|
3989
4603
|
width: "50%",
|
|
3990
4604
|
flexDirection: "column",
|
|
3991
4605
|
paddingX: 1,
|
|
@@ -3993,7 +4607,7 @@ async function launchBrowser(theme_override) {
|
|
|
3993
4607
|
focusable: false,
|
|
3994
4608
|
backgroundColor: colors.surface
|
|
3995
4609
|
});
|
|
3996
|
-
const detailText = new
|
|
4610
|
+
const detailText = new TextRenderable5(renderer, {
|
|
3997
4611
|
content: "",
|
|
3998
4612
|
fg: colors.text,
|
|
3999
4613
|
wrapMode: "none"
|
|
@@ -4002,7 +4616,7 @@ async function launchBrowser(theme_override) {
|
|
|
4002
4616
|
body.add(resultsBox);
|
|
4003
4617
|
body.add(detailBox);
|
|
4004
4618
|
root.add(body);
|
|
4005
|
-
const landingBox = new
|
|
4619
|
+
const landingBox = new BoxRenderable5(renderer, {
|
|
4006
4620
|
visible: false,
|
|
4007
4621
|
flexGrow: 1,
|
|
4008
4622
|
flexDirection: "column",
|
|
@@ -4010,7 +4624,7 @@ async function launchBrowser(theme_override) {
|
|
|
4010
4624
|
justifyContent: "center"
|
|
4011
4625
|
});
|
|
4012
4626
|
root.add(landingBox);
|
|
4013
|
-
const landingBanner = new
|
|
4627
|
+
const landingBanner = new TextRenderable5(renderer, {
|
|
4014
4628
|
content: [
|
|
4015
4629
|
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
4016
4630
|
"\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
@@ -4060,7 +4674,7 @@ async function launchBrowser(theme_override) {
|
|
|
4060
4674
|
const landingCards = [];
|
|
4061
4675
|
for (let i = 0; i < landingOptions.length; i++) {
|
|
4062
4676
|
const opt = landingOptions[i];
|
|
4063
|
-
const card = new
|
|
4677
|
+
const card = new BoxRenderable5(renderer, {
|
|
4064
4678
|
flexDirection: "column",
|
|
4065
4679
|
height: 5,
|
|
4066
4680
|
width: "60%",
|
|
@@ -4072,18 +4686,18 @@ async function launchBrowser(theme_override) {
|
|
|
4072
4686
|
});
|
|
4073
4687
|
landingCards.push(card);
|
|
4074
4688
|
landingBox.add(card);
|
|
4075
|
-
const spacerTop = new
|
|
4076
|
-
const titleLine = new
|
|
4689
|
+
const spacerTop = new TextRenderable5(renderer, { content: "", height: 1 });
|
|
4690
|
+
const titleLine = new TextRenderable5(renderer, {
|
|
4077
4691
|
content: opt.title,
|
|
4078
4692
|
fg: i === 0 ? colors.accent : colors.text,
|
|
4079
4693
|
height: 1
|
|
4080
4694
|
});
|
|
4081
|
-
const spacerBot = new
|
|
4695
|
+
const spacerBot = new TextRenderable5(renderer, { content: "", height: 1 });
|
|
4082
4696
|
card.add(spacerTop);
|
|
4083
4697
|
card.add(titleLine);
|
|
4084
4698
|
card.add(spacerBot);
|
|
4085
4699
|
}
|
|
4086
|
-
const landingHint = new
|
|
4700
|
+
const landingHint = new TextRenderable5(renderer, {
|
|
4087
4701
|
content: " \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
|
|
4088
4702
|
fg: colors.muted,
|
|
4089
4703
|
bg: colors.bg,
|
|
@@ -4226,7 +4840,7 @@ async function launchBrowser(theme_override) {
|
|
|
4226
4840
|
renderer.requestRender();
|
|
4227
4841
|
}
|
|
4228
4842
|
const helpSections = buildHelpSections();
|
|
4229
|
-
const helpBox = new
|
|
4843
|
+
const helpBox = new BoxRenderable5(renderer, {
|
|
4230
4844
|
visible: false,
|
|
4231
4845
|
flexGrow: 1,
|
|
4232
4846
|
backgroundColor: colors.bg
|
|
@@ -4248,7 +4862,7 @@ async function launchBrowser(theme_override) {
|
|
|
4248
4862
|
}
|
|
4249
4863
|
});
|
|
4250
4864
|
for (const section of helpSections) {
|
|
4251
|
-
const sectionBox = new
|
|
4865
|
+
const sectionBox = new BoxRenderable5(renderer, {
|
|
4252
4866
|
title: section.title,
|
|
4253
4867
|
titleColor: colors[section.titleColor] ?? section.titleColor,
|
|
4254
4868
|
flexDirection: "column",
|
|
@@ -4260,7 +4874,7 @@ async function launchBrowser(theme_override) {
|
|
|
4260
4874
|
for (const row of section.rows) {
|
|
4261
4875
|
const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
|
|
4262
4876
|
sectionBox.add(
|
|
4263
|
-
new
|
|
4877
|
+
new TextRenderable5(renderer, {
|
|
4264
4878
|
content: ` ${keys} ${row.action}`,
|
|
4265
4879
|
fg: colors.text
|
|
4266
4880
|
})
|
|
@@ -4268,7 +4882,7 @@ async function launchBrowser(theme_override) {
|
|
|
4268
4882
|
}
|
|
4269
4883
|
if (section.note) {
|
|
4270
4884
|
sectionBox.add(
|
|
4271
|
-
new
|
|
4885
|
+
new TextRenderable5(renderer, {
|
|
4272
4886
|
content: ` ${section.note}`,
|
|
4273
4887
|
fg: colors.muted
|
|
4274
4888
|
})
|
|
@@ -4278,7 +4892,7 @@ async function launchBrowser(theme_override) {
|
|
|
4278
4892
|
}
|
|
4279
4893
|
helpBox.add(helpScroll);
|
|
4280
4894
|
root.add(helpBox);
|
|
4281
|
-
const historyBox = new
|
|
4895
|
+
const historyBox = new BoxRenderable5(renderer, {
|
|
4282
4896
|
visible: false,
|
|
4283
4897
|
flexGrow: 1,
|
|
4284
4898
|
backgroundColor: colors.bg,
|
|
@@ -4313,7 +4927,7 @@ async function launchBrowser(theme_override) {
|
|
|
4313
4927
|
}
|
|
4314
4928
|
historySelect.setSelectedIndex(0);
|
|
4315
4929
|
}
|
|
4316
|
-
const bookmarksBox = new
|
|
4930
|
+
const bookmarksBox = new BoxRenderable5(renderer, {
|
|
4317
4931
|
visible: false,
|
|
4318
4932
|
flexGrow: 1,
|
|
4319
4933
|
backgroundColor: colors.bg,
|
|
@@ -4348,7 +4962,7 @@ async function launchBrowser(theme_override) {
|
|
|
4348
4962
|
}
|
|
4349
4963
|
bookmarksSelect.setSelectedIndex(0);
|
|
4350
4964
|
}
|
|
4351
|
-
const savedBox = new
|
|
4965
|
+
const savedBox = new BoxRenderable5(renderer, {
|
|
4352
4966
|
visible: false,
|
|
4353
4967
|
flexGrow: 1,
|
|
4354
4968
|
backgroundColor: colors.bg,
|
|
@@ -4368,7 +4982,7 @@ async function launchBrowser(theme_override) {
|
|
|
4368
4982
|
});
|
|
4369
4983
|
savedBox.add(savedSelect);
|
|
4370
4984
|
root.add(savedBox);
|
|
4371
|
-
const topicsBox = new
|
|
4985
|
+
const topicsBox = new BoxRenderable5(renderer, {
|
|
4372
4986
|
visible: false,
|
|
4373
4987
|
flexGrow: 1,
|
|
4374
4988
|
backgroundColor: colors.bg,
|
|
@@ -4403,7 +5017,7 @@ async function launchBrowser(theme_override) {
|
|
|
4403
5017
|
}
|
|
4404
5018
|
topicsSelect.setSelectedIndex(0);
|
|
4405
5019
|
}
|
|
4406
|
-
const exportBox = new
|
|
5020
|
+
const exportBox = new BoxRenderable5(renderer, {
|
|
4407
5021
|
visible: false,
|
|
4408
5022
|
flexGrow: 1,
|
|
4409
5023
|
backgroundColor: colors.bg,
|
|
@@ -4440,28 +5054,28 @@ async function launchBrowser(theme_override) {
|
|
|
4440
5054
|
});
|
|
4441
5055
|
exportBox.add(exportSelect);
|
|
4442
5056
|
root.add(exportBox);
|
|
4443
|
-
const compareBox = new
|
|
5057
|
+
const compareBox = new BoxRenderable5(renderer, {
|
|
4444
5058
|
visible: false,
|
|
4445
5059
|
flexGrow: 1,
|
|
4446
5060
|
backgroundColor: colors.bg,
|
|
4447
5061
|
title: " Comparison ",
|
|
4448
5062
|
titleColor: colors.yellow
|
|
4449
5063
|
});
|
|
4450
|
-
const compareText = new
|
|
5064
|
+
const compareText = new TextRenderable5(renderer, {
|
|
4451
5065
|
content: "",
|
|
4452
5066
|
fg: colors.text,
|
|
4453
5067
|
bg: colors.bg
|
|
4454
5068
|
});
|
|
4455
5069
|
compareBox.add(compareText);
|
|
4456
5070
|
root.add(compareBox);
|
|
4457
|
-
const notifsBox = new
|
|
5071
|
+
const notifsBox = new BoxRenderable5(renderer, {
|
|
4458
5072
|
visible: false,
|
|
4459
5073
|
flexGrow: 1,
|
|
4460
5074
|
backgroundColor: colors.bg,
|
|
4461
5075
|
title: " Notifications ",
|
|
4462
5076
|
titleColor: colors.yellow
|
|
4463
5077
|
});
|
|
4464
|
-
const notifsText = new
|
|
5078
|
+
const notifsText = new TextRenderable5(renderer, {
|
|
4465
5079
|
content: "(no notifications)",
|
|
4466
5080
|
fg: colors.text,
|
|
4467
5081
|
bg: colors.bg
|
|
@@ -4476,7 +5090,7 @@ async function launchBrowser(theme_override) {
|
|
|
4476
5090
|
notifsText.content = notifs2.map((n) => ` ${n.icon} ${n.message}`).join("\n");
|
|
4477
5091
|
}
|
|
4478
5092
|
}
|
|
4479
|
-
const shareBox = new
|
|
5093
|
+
const shareBox = new BoxRenderable5(renderer, {
|
|
4480
5094
|
visible: false,
|
|
4481
5095
|
flexGrow: 1,
|
|
4482
5096
|
backgroundColor: colors.bg,
|
|
@@ -4535,7 +5149,7 @@ async function launchBrowser(theme_override) {
|
|
|
4535
5149
|
}
|
|
4536
5150
|
savedSelect.setSelectedIndex(0);
|
|
4537
5151
|
}
|
|
4538
|
-
const leaderDim = new
|
|
5152
|
+
const leaderDim = new BoxRenderable5(renderer, {
|
|
4539
5153
|
visible: false,
|
|
4540
5154
|
position: "absolute",
|
|
4541
5155
|
top: 0,
|
|
@@ -4546,7 +5160,7 @@ async function launchBrowser(theme_override) {
|
|
|
4546
5160
|
// 60% opacity black (hex alpha: 0x99 ≈ 153/255)
|
|
4547
5161
|
});
|
|
4548
5162
|
root.add(leaderDim);
|
|
4549
|
-
const leaderBox = new
|
|
5163
|
+
const leaderBox = new BoxRenderable5(renderer, {
|
|
4550
5164
|
visible: false,
|
|
4551
5165
|
position: "absolute",
|
|
4552
5166
|
width: "80%",
|
|
@@ -4578,7 +5192,7 @@ async function launchBrowser(theme_override) {
|
|
|
4578
5192
|
selectedDescriptionColor: colors.selectionText,
|
|
4579
5193
|
itemSpacing: 0
|
|
4580
5194
|
});
|
|
4581
|
-
const leaderFooter = new
|
|
5195
|
+
const leaderFooter = new TextRenderable5(renderer, {
|
|
4582
5196
|
content: " \u2191\u2193 select Enter run Esc/q close ",
|
|
4583
5197
|
fg: colors.muted,
|
|
4584
5198
|
bg: colors.surfaceDim,
|
|
@@ -4589,7 +5203,7 @@ async function launchBrowser(theme_override) {
|
|
|
4589
5203
|
leaderBox.add(leaderFooter);
|
|
4590
5204
|
root.add(leaderDim);
|
|
4591
5205
|
root.add(leaderBox);
|
|
4592
|
-
const updateDim = new
|
|
5206
|
+
const updateDim = new BoxRenderable5(renderer, {
|
|
4593
5207
|
visible: false,
|
|
4594
5208
|
position: "absolute",
|
|
4595
5209
|
top: 0,
|
|
@@ -4599,7 +5213,7 @@ async function launchBrowser(theme_override) {
|
|
|
4599
5213
|
backgroundColor: "#00000088"
|
|
4600
5214
|
});
|
|
4601
5215
|
root.add(updateDim);
|
|
4602
|
-
const updateBox = new
|
|
5216
|
+
const updateBox = new BoxRenderable5(renderer, {
|
|
4603
5217
|
visible: false,
|
|
4604
5218
|
position: "absolute",
|
|
4605
5219
|
width: "55%",
|
|
@@ -4618,11 +5232,19 @@ async function launchBrowser(theme_override) {
|
|
|
4618
5232
|
paddingLeft: 2,
|
|
4619
5233
|
paddingRight: 2
|
|
4620
5234
|
});
|
|
4621
|
-
const
|
|
5235
|
+
const updateHeader = new TextRenderable5(renderer, {
|
|
4622
5236
|
content: "",
|
|
4623
5237
|
fg: colors.accent,
|
|
4624
5238
|
bg: colors.surfaceDim
|
|
4625
5239
|
});
|
|
5240
|
+
const updateNotes = createMarkdownView({
|
|
5241
|
+
renderer,
|
|
5242
|
+
colors,
|
|
5243
|
+
// Notes ship with the release and are mostly prose/lists; skip the
|
|
5244
|
+
// image pipeline in this narrow panel.
|
|
5245
|
+
images: false,
|
|
5246
|
+
background: colors.surfaceDim
|
|
5247
|
+
});
|
|
4626
5248
|
const updateScroll = new ScrollBoxRenderable3(renderer, {
|
|
4627
5249
|
flexGrow: 1,
|
|
4628
5250
|
backgroundColor: colors.surfaceDim,
|
|
@@ -4642,14 +5264,14 @@ async function launchBrowser(theme_override) {
|
|
|
4642
5264
|
}
|
|
4643
5265
|
});
|
|
4644
5266
|
let updateSelectedOption = 0;
|
|
4645
|
-
const updateOptionsRow = new
|
|
5267
|
+
const updateOptionsRow = new BoxRenderable5(renderer, {
|
|
4646
5268
|
flexDirection: "row",
|
|
4647
5269
|
gap: 1,
|
|
4648
5270
|
backgroundColor: colors.surfaceDim,
|
|
4649
5271
|
height: 3,
|
|
4650
5272
|
width: "100%"
|
|
4651
5273
|
});
|
|
4652
|
-
const updateNowBox = new
|
|
5274
|
+
const updateNowBox = new BoxRenderable5(renderer, {
|
|
4653
5275
|
border: true,
|
|
4654
5276
|
borderColor: updateSelectedOption === 0 ? colors.yellow : colors.border,
|
|
4655
5277
|
backgroundColor: colors.surfaceDim,
|
|
@@ -4658,13 +5280,13 @@ async function launchBrowser(theme_override) {
|
|
|
4658
5280
|
paddingLeft: 1,
|
|
4659
5281
|
paddingRight: 1
|
|
4660
5282
|
});
|
|
4661
|
-
const updateNowText = new
|
|
5283
|
+
const updateNowText = new TextRenderable5(renderer, {
|
|
4662
5284
|
content: " Update Now ",
|
|
4663
5285
|
fg: updateSelectedOption === 0 ? colors.yellow : colors.text,
|
|
4664
5286
|
bg: colors.surfaceDim
|
|
4665
5287
|
});
|
|
4666
5288
|
updateNowBox.add(updateNowText);
|
|
4667
|
-
const laterBox = new
|
|
5289
|
+
const laterBox = new BoxRenderable5(renderer, {
|
|
4668
5290
|
border: true,
|
|
4669
5291
|
borderColor: updateSelectedOption === 1 ? colors.yellow : colors.border,
|
|
4670
5292
|
backgroundColor: colors.surfaceDim,
|
|
@@ -4673,13 +5295,13 @@ async function launchBrowser(theme_override) {
|
|
|
4673
5295
|
paddingLeft: 1,
|
|
4674
5296
|
paddingRight: 1
|
|
4675
5297
|
});
|
|
4676
|
-
const laterText = new
|
|
5298
|
+
const laterText = new TextRenderable5(renderer, {
|
|
4677
5299
|
content: " Later",
|
|
4678
5300
|
fg: updateSelectedOption === 1 ? colors.yellow : colors.text,
|
|
4679
5301
|
bg: colors.surfaceDim
|
|
4680
5302
|
});
|
|
4681
5303
|
laterBox.add(laterText);
|
|
4682
|
-
const neverBox = new
|
|
5304
|
+
const neverBox = new BoxRenderable5(renderer, {
|
|
4683
5305
|
border: true,
|
|
4684
5306
|
borderColor: updateSelectedOption === 2 ? colors.yellow : colors.border,
|
|
4685
5307
|
backgroundColor: colors.surfaceDim,
|
|
@@ -4688,7 +5310,7 @@ async function launchBrowser(theme_override) {
|
|
|
4688
5310
|
paddingLeft: 1,
|
|
4689
5311
|
paddingRight: 1
|
|
4690
5312
|
});
|
|
4691
|
-
const neverText = new
|
|
5313
|
+
const neverText = new TextRenderable5(renderer, {
|
|
4692
5314
|
content: " Don't show again",
|
|
4693
5315
|
fg: updateSelectedOption === 2 ? colors.yellow : colors.text,
|
|
4694
5316
|
bg: colors.surfaceDim
|
|
@@ -4705,7 +5327,7 @@ async function launchBrowser(theme_override) {
|
|
|
4705
5327
|
}
|
|
4706
5328
|
updateOptionsRow.add(updateNowBox);
|
|
4707
5329
|
updateOptionsRow.add(laterBox);
|
|
4708
|
-
const updateFooter = new
|
|
5330
|
+
const updateFooter = new TextRenderable5(renderer, {
|
|
4709
5331
|
content: " \u2191\u2193/jk/PgUp/PgDn scroll \u2190\u2192 select Enter Esc/q close",
|
|
4710
5332
|
fg: colors.muted,
|
|
4711
5333
|
bg: colors.surfaceDim,
|
|
@@ -4713,7 +5335,8 @@ async function launchBrowser(theme_override) {
|
|
|
4713
5335
|
paddingX: 1
|
|
4714
5336
|
});
|
|
4715
5337
|
updateBox.add(updateScroll);
|
|
4716
|
-
updateScroll.add(
|
|
5338
|
+
updateScroll.add(updateHeader);
|
|
5339
|
+
updateScroll.add(updateNotes.renderable);
|
|
4717
5340
|
updateBox.add(updateOptionsRow);
|
|
4718
5341
|
updateBox.add(updateFooter);
|
|
4719
5342
|
root.add(updateDim);
|
|
@@ -5061,15 +5684,12 @@ async function launchBrowser(theme_override) {
|
|
|
5061
5684
|
if (!state.lastInstalledVersion) return;
|
|
5062
5685
|
if (state.lastInstalledVersion === currentVersion) return;
|
|
5063
5686
|
const notes = fetchReleaseNotes(currentVersion);
|
|
5064
|
-
const notesWidth = Math.max(
|
|
5065
|
-
30,
|
|
5066
|
-
Math.floor((process.stdout.columns || 80) * 0.55) - 8
|
|
5067
|
-
);
|
|
5068
|
-
const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : " No release notes found for this version.";
|
|
5069
5687
|
updateSelectedOption = 0;
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5688
|
+
updateHeader.content = ` Updated ghfind ${state.lastInstalledVersion} \u2192 ${currentVersion}
|
|
5689
|
+
`;
|
|
5690
|
+
updateNotes.setContent(
|
|
5691
|
+
notes ?? "_No release notes found for this version._"
|
|
5692
|
+
);
|
|
5073
5693
|
renderUpdateOptions();
|
|
5074
5694
|
showOverlay("update");
|
|
5075
5695
|
}
|
|
@@ -5084,22 +5704,12 @@ ${notesBlock}`;
|
|
|
5084
5704
|
if (latest) {
|
|
5085
5705
|
updateSelectedOption = 0;
|
|
5086
5706
|
const notes = fetchReleaseNotes(latest);
|
|
5087
|
-
|
|
5088
|
-
30,
|
|
5089
|
-
Math.floor((process.stdout.columns || 80) * 0.55) - 8
|
|
5090
|
-
);
|
|
5091
|
-
const notesBlock = notes ? renderMarkdown(notes, { width: notesWidth }) : ` A new version is available
|
|
5092
|
-
|
|
5093
|
-
Current: ${currentVersion}
|
|
5094
|
-
Latest: ${latest}
|
|
5095
|
-
|
|
5096
|
-
Run "npm install -g ghfind"`;
|
|
5097
|
-
updateText.content = ` A new version is available
|
|
5707
|
+
updateHeader.content = ` A new version is available
|
|
5098
5708
|
|
|
5099
5709
|
Current: ${currentVersion}
|
|
5100
5710
|
Latest: ${latest}
|
|
5101
|
-
|
|
5102
|
-
|
|
5711
|
+
`;
|
|
5712
|
+
updateNotes.setContent(notes ?? "Run `npm install -g ghfind` to update.");
|
|
5103
5713
|
renderUpdateOptions();
|
|
5104
5714
|
showOverlay("update");
|
|
5105
5715
|
} else {
|
|
@@ -5182,7 +5792,7 @@ ${notesBlock}`;
|
|
|
5182
5792
|
renderer.requestRender();
|
|
5183
5793
|
}
|
|
5184
5794
|
}
|
|
5185
|
-
const orgBox = new
|
|
5795
|
+
const orgBox = new BoxRenderable5(renderer, {
|
|
5186
5796
|
visible: false,
|
|
5187
5797
|
flexGrow: 1,
|
|
5188
5798
|
backgroundColor: colors.surface,
|
|
@@ -5205,14 +5815,14 @@ ${notesBlock}`;
|
|
|
5205
5815
|
}
|
|
5206
5816
|
}
|
|
5207
5817
|
});
|
|
5208
|
-
const orgText = new
|
|
5818
|
+
const orgText = new TextRenderable5(renderer, {
|
|
5209
5819
|
content: "",
|
|
5210
5820
|
fg: colors.text,
|
|
5211
5821
|
bg: colors.bg
|
|
5212
5822
|
});
|
|
5213
5823
|
orgScroll.add(orgText);
|
|
5214
5824
|
orgBox.add(orgScroll);
|
|
5215
|
-
const orgFooter = new
|
|
5825
|
+
const orgFooter = new TextRenderable5(renderer, {
|
|
5216
5826
|
content: " \u2191\u2193/jk scroll Esc/q close",
|
|
5217
5827
|
fg: colors.muted,
|
|
5218
5828
|
bg: colors.bg,
|
|
@@ -5278,7 +5888,7 @@ ${notesBlock}`;
|
|
|
5278
5888
|
}
|
|
5279
5889
|
renderer.requestRender();
|
|
5280
5890
|
}
|
|
5281
|
-
const readmeBox = new
|
|
5891
|
+
const readmeBox = new BoxRenderable5(renderer, {
|
|
5282
5892
|
visible: false,
|
|
5283
5893
|
flexGrow: 1,
|
|
5284
5894
|
backgroundColor: colors.surface,
|
|
@@ -5301,15 +5911,17 @@ ${notesBlock}`;
|
|
|
5301
5911
|
}
|
|
5302
5912
|
}
|
|
5303
5913
|
});
|
|
5304
|
-
const
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5914
|
+
const readmeView = createMarkdownView({
|
|
5915
|
+
renderer,
|
|
5916
|
+
colors,
|
|
5917
|
+
loadImage: (url) => fetchImageBytes(url, { token: githubToken }),
|
|
5918
|
+
getImageWidth: () => Math.max(24, (process.stdout.columns || 80) - 12)
|
|
5308
5919
|
});
|
|
5309
|
-
readmeScroll.add(
|
|
5920
|
+
readmeScroll.add(readmeView.renderable);
|
|
5310
5921
|
readmeBox.add(readmeScroll);
|
|
5311
|
-
const
|
|
5312
|
-
|
|
5922
|
+
const readmeFooterText = () => ` \u2191\u2193/jk scroll PgUp/PgDn page i images: ${readmeView.imagesEnabled ? "on" : "off"} Esc/q close`;
|
|
5923
|
+
const readmeFooter = new TextRenderable5(renderer, {
|
|
5924
|
+
content: readmeFooterText(),
|
|
5313
5925
|
fg: colors.muted,
|
|
5314
5926
|
bg: colors.bg,
|
|
5315
5927
|
height: 1,
|
|
@@ -5324,37 +5936,25 @@ ${notesBlock}`;
|
|
|
5324
5936
|
setStatus("No repo selected");
|
|
5325
5937
|
return;
|
|
5326
5938
|
}
|
|
5327
|
-
|
|
5939
|
+
readmeView.setContent(`Loading README for ${repo.fullName}\u2026`);
|
|
5940
|
+
readmeFooter.content = readmeFooterText();
|
|
5941
|
+
readmeScroll.scrollTop = 0;
|
|
5328
5942
|
showOverlay("readme");
|
|
5329
5943
|
try {
|
|
5330
|
-
const
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
`${repo.owner}/${repo.name}/master/README.md`,
|
|
5336
|
-
`${repo.owner}/${repo.name}/main/README.rst`
|
|
5337
|
-
]) {
|
|
5338
|
-
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
5339
|
-
headers
|
|
5340
|
-
});
|
|
5341
|
-
if (r.ok) {
|
|
5342
|
-
text = await r.text();
|
|
5343
|
-
break;
|
|
5344
|
-
}
|
|
5345
|
-
}
|
|
5346
|
-
if (!text) {
|
|
5347
|
-
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
5944
|
+
const readme = await fetchReadme(repo.owner, repo.name, {
|
|
5945
|
+
token: githubToken
|
|
5946
|
+
});
|
|
5947
|
+
if (readme) {
|
|
5948
|
+
readmeView.setContent(readme.text, readme.sourceUrl);
|
|
5348
5949
|
} else {
|
|
5349
|
-
|
|
5350
|
-
readmeText.content = renderMarkdown(text, { width: termW });
|
|
5950
|
+
readmeView.setContent(`_(no README found for ${repo.fullName})_`);
|
|
5351
5951
|
}
|
|
5352
5952
|
} catch {
|
|
5353
|
-
|
|
5953
|
+
readmeView.setContent("_Failed to load README._");
|
|
5354
5954
|
}
|
|
5355
5955
|
renderer.requestRender();
|
|
5356
5956
|
}
|
|
5357
|
-
const statusBar = new
|
|
5957
|
+
const statusBar = new TextRenderable5(renderer, {
|
|
5358
5958
|
content: " Ready. Press Enter to search.",
|
|
5359
5959
|
fg: colors.muted,
|
|
5360
5960
|
height: 1,
|
|
@@ -5835,6 +6435,17 @@ ${notesBlock}`;
|
|
|
5835
6435
|
doSearch(searchInput.value);
|
|
5836
6436
|
}
|
|
5837
6437
|
});
|
|
6438
|
+
searchInput.on("input", () => {
|
|
6439
|
+
const v = searchInput.value;
|
|
6440
|
+
if (v.length > 0) {
|
|
6441
|
+
const t3 = v.trim();
|
|
6442
|
+
if (t3.startsWith("{") && t3.endsWith("}") || t3.startsWith("[") && t3.endsWith("]")) {
|
|
6443
|
+
searchInput.value = "";
|
|
6444
|
+
setStatus("Ignored JSON paste \u2014 search queries accept plain text");
|
|
6445
|
+
renderer.requestRender();
|
|
6446
|
+
}
|
|
6447
|
+
}
|
|
6448
|
+
});
|
|
5838
6449
|
resultsSelect.on("selectionChanged", () => {
|
|
5839
6450
|
const opt = resultsSelect.getSelectedOption();
|
|
5840
6451
|
if (currentMode === "packages") {
|
|
@@ -5917,6 +6528,14 @@ ${pack.description ?? ""}`;
|
|
|
5917
6528
|
return;
|
|
5918
6529
|
}
|
|
5919
6530
|
if (currentOverlay === "readme") {
|
|
6531
|
+
if (key.name === "i") {
|
|
6532
|
+
readmeView.setImagesEnabled(!readmeView.imagesEnabled);
|
|
6533
|
+
readmeFooter.content = readmeFooterText();
|
|
6534
|
+
setStatus(
|
|
6535
|
+
readmeView.imagesEnabled ? "README images on" : "README images off"
|
|
6536
|
+
);
|
|
6537
|
+
renderer.requestRender();
|
|
6538
|
+
}
|
|
5920
6539
|
return;
|
|
5921
6540
|
}
|
|
5922
6541
|
if (currentOverlay === "history") {
|
|
@@ -6350,6 +6969,12 @@ reported in text output and via the error message.
|
|
|
6350
6969
|
ghfind deep-dive owner/repo --json
|
|
6351
6970
|
Text output (no flag) prints the same sections formatted for the terminal.
|
|
6352
6971
|
|
|
6972
|
+
## Finding skills
|
|
6973
|
+
ghfind skill search QUERY # skills installed on this machine
|
|
6974
|
+
ghfind skill search QUERY --remote # the public skills.sh ecosystem
|
|
6975
|
+
Results carry path+description (local) or install command (registry, read-only --
|
|
6976
|
+
nothing is installed). The MCP server exposes the same search as
|
|
6977
|
+
ghfind_skill_search.
|
|
6353
6978
|
## Rate limits
|
|
6354
6979
|
|
|
6355
6980
|
60 req/hr unauthenticated, 5,000 with GITHUB_TOKEN (env var or \`ghfind login\`).
|
|
@@ -6361,6 +6986,14 @@ Bookmarks live in ghfind's local state, not GitHub. An agent can read the
|
|
|
6361
6986
|
release feed with:
|
|
6362
6987
|
ghfind --releases --json
|
|
6363
6988
|
Marking releases seen and editing bookmarks is a TUI action; skip it.
|
|
6989
|
+
## Local state (read-only)
|
|
6990
|
+
ghfind bookmarks [QUERY] --json # saved repos, newest first
|
|
6991
|
+
ghfind history [QUERY] --json # past searches
|
|
6992
|
+
ghfind saved --json # named searches
|
|
6993
|
+
ghfind topics --json # popular GitHub topics
|
|
6994
|
+
ghfind readme owner/repo # print a repo README
|
|
6995
|
+
ghfind share owner/repo --as gh-cli|markdown|plain|short
|
|
6996
|
+
These never write local state; skip them when nothing needs reading.
|
|
6364
6997
|
|
|
6365
6998
|
## Token efficiency
|
|
6366
6999
|
|
|
@@ -6400,6 +7033,233 @@ function formatSkillCatalog() {
|
|
|
6400
7033
|
].join("\n");
|
|
6401
7034
|
}
|
|
6402
7035
|
|
|
7036
|
+
// src/skill-finder.ts
|
|
7037
|
+
import { readdirSync, readFileSync as readFileSync6 } from "fs";
|
|
7038
|
+
import { homedir as homedir3 } from "os";
|
|
7039
|
+
import { delimiter, join as join6 } from "path";
|
|
7040
|
+
var REGISTRY_SEARCH_ENDPOINT = "https://skills.sh/api/search";
|
|
7041
|
+
var MIN_QUERY_LENGTH = 2;
|
|
7042
|
+
var MAX_SKILL_RESULTS = 100;
|
|
7043
|
+
function displayPath(path) {
|
|
7044
|
+
const home = homedir3();
|
|
7045
|
+
const normalized = path.replace(/\\/g, "/");
|
|
7046
|
+
const homeNorm = home.replace(/\\/g, "/").replace(/\/$/, "");
|
|
7047
|
+
if (normalized === homeNorm) return "~";
|
|
7048
|
+
if (normalized.startsWith(`${homeNorm}/`)) {
|
|
7049
|
+
return `~${normalized.slice(homeNorm.length)}`;
|
|
7050
|
+
}
|
|
7051
|
+
return normalized;
|
|
7052
|
+
}
|
|
7053
|
+
function defaultSkillRoots(cwd = process.cwd()) {
|
|
7054
|
+
const home = homedir3();
|
|
7055
|
+
return [
|
|
7056
|
+
join6(home, ".codex", "skills"),
|
|
7057
|
+
join6(home, ".agents", "skills"),
|
|
7058
|
+
join6(cwd, ".codex", "skills"),
|
|
7059
|
+
join6(cwd, ".github", "skills")
|
|
7060
|
+
];
|
|
7061
|
+
}
|
|
7062
|
+
function resolveSkillRoots(cwd = process.cwd()) {
|
|
7063
|
+
const override = process.env.GHFIND_SKILL_ROOTS;
|
|
7064
|
+
const paths = override?.trim() ? override.split(delimiter).filter((p) => p.trim()).map((p) => p.trim()) : defaultSkillRoots(cwd);
|
|
7065
|
+
return paths.map((path) => ({ path, label: displayPath(path) }));
|
|
7066
|
+
}
|
|
7067
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
7068
|
+
var MAX_DEPTH = 4;
|
|
7069
|
+
function unquote(value) {
|
|
7070
|
+
const trimmed = value.trim();
|
|
7071
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
7072
|
+
return trimmed.slice(1, -1).trim();
|
|
7073
|
+
}
|
|
7074
|
+
return trimmed;
|
|
7075
|
+
}
|
|
7076
|
+
function parseSkillFrontmatter(text) {
|
|
7077
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
7078
|
+
if (!match) return null;
|
|
7079
|
+
const lines = match[1].split(/\r?\n/);
|
|
7080
|
+
const fields = {};
|
|
7081
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7082
|
+
const field = /^([A-Za-z_][\w-]*):[ \t]*(.*)$/.exec(lines[i]);
|
|
7083
|
+
if (!field) continue;
|
|
7084
|
+
const key = field[1].toLowerCase();
|
|
7085
|
+
const inline = field[2].trim();
|
|
7086
|
+
if (inline === ">" || inline === "|" || /^[>|][-+]?$/.test(inline)) {
|
|
7087
|
+
const literal = inline.startsWith("|");
|
|
7088
|
+
const parts = [];
|
|
7089
|
+
while (i + 1 < lines.length && /^[ \t]+\S/.test(lines[i + 1])) {
|
|
7090
|
+
parts.push(lines[++i].trim());
|
|
7091
|
+
}
|
|
7092
|
+
fields[key] = literal ? parts.join("\n") : parts.join(" ");
|
|
7093
|
+
} else {
|
|
7094
|
+
fields[key] = unquote(inline);
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
const name = fields.name ?? "";
|
|
7098
|
+
if (!name) return null;
|
|
7099
|
+
return { name, description: fields.description ?? "" };
|
|
7100
|
+
}
|
|
7101
|
+
function scanLocalSkills(roots = resolveSkillRoots()) {
|
|
7102
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7103
|
+
for (const root of roots) {
|
|
7104
|
+
for (const dir of walkDirs(root.path)) {
|
|
7105
|
+
const entries = listDir(dir);
|
|
7106
|
+
if (!entries) continue;
|
|
7107
|
+
if (!entries.some((e) => e.isFile() && e.name === "SKILL.md")) continue;
|
|
7108
|
+
const file = join6(dir, "SKILL.md");
|
|
7109
|
+
try {
|
|
7110
|
+
const parsed = parseSkillFrontmatter(readFileSync6(file, "utf-8"));
|
|
7111
|
+
if (!parsed || byName.has(parsed.name)) continue;
|
|
7112
|
+
byName.set(parsed.name, {
|
|
7113
|
+
name: parsed.name,
|
|
7114
|
+
description: parsed.description.replace(/\s+/g, " ").trim(),
|
|
7115
|
+
path: file,
|
|
7116
|
+
origin: root.label
|
|
7117
|
+
});
|
|
7118
|
+
} catch {
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
return [...byName.values()];
|
|
7123
|
+
}
|
|
7124
|
+
function listDir(dir) {
|
|
7125
|
+
try {
|
|
7126
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
7127
|
+
} catch {
|
|
7128
|
+
return null;
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
function* walkDirs(root) {
|
|
7132
|
+
const queue = [[root, 0]];
|
|
7133
|
+
while (queue.length > 0) {
|
|
7134
|
+
const [dir, depth] = queue.shift();
|
|
7135
|
+
const entries = listDir(dir);
|
|
7136
|
+
if (!entries) continue;
|
|
7137
|
+
yield dir;
|
|
7138
|
+
if (depth >= MAX_DEPTH) continue;
|
|
7139
|
+
for (const entry of entries) {
|
|
7140
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
7141
|
+
queue.push([join6(dir, entry.name), depth + 1]);
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
function localMatchTier(hit, needle) {
|
|
7146
|
+
const name = hit.name.toLowerCase();
|
|
7147
|
+
if (name === needle) return 0;
|
|
7148
|
+
if (name.startsWith(needle)) return 1;
|
|
7149
|
+
if (name.includes(needle)) return 2;
|
|
7150
|
+
if (hit.description.toLowerCase().includes(needle)) return 3;
|
|
7151
|
+
return null;
|
|
7152
|
+
}
|
|
7153
|
+
function rankLocalSkills(hits, query, limit = MAX_SKILL_RESULTS) {
|
|
7154
|
+
const needle = query.trim().toLowerCase();
|
|
7155
|
+
return hits.map((hit) => ({ hit, tier: localMatchTier(hit, needle) })).filter(
|
|
7156
|
+
(entry) => entry.tier !== null
|
|
7157
|
+
).sort((a, b) => a.tier - b.tier || a.hit.name.localeCompare(b.hit.name)).slice(0, clampLimit(limit)).map((entry) => entry.hit);
|
|
7158
|
+
}
|
|
7159
|
+
function clampLimit(limit) {
|
|
7160
|
+
const n = Math.trunc(Number(limit));
|
|
7161
|
+
if (!Number.isFinite(n)) return MAX_SKILL_RESULTS;
|
|
7162
|
+
return Math.min(Math.max(n, 1), MAX_SKILL_RESULTS);
|
|
7163
|
+
}
|
|
7164
|
+
function assertSearchableQuery(query) {
|
|
7165
|
+
const trimmed = query.trim();
|
|
7166
|
+
if (trimmed.length < MIN_QUERY_LENGTH) {
|
|
7167
|
+
throw new Error(
|
|
7168
|
+
`Query must be at least ${MIN_QUERY_LENGTH} characters (got "${trimmed}").`
|
|
7169
|
+
);
|
|
7170
|
+
}
|
|
7171
|
+
return trimmed;
|
|
7172
|
+
}
|
|
7173
|
+
function searchLocalSkills(query, opts = {}) {
|
|
7174
|
+
const trimmed = assertSearchableQuery(query);
|
|
7175
|
+
const skills = rankLocalSkills(
|
|
7176
|
+
scanLocalSkills(opts.roots ?? resolveSkillRoots()),
|
|
7177
|
+
trimmed,
|
|
7178
|
+
opts.limit ?? MAX_SKILL_RESULTS
|
|
7179
|
+
);
|
|
7180
|
+
return { query: trimmed, source: "local", count: skills.length, skills };
|
|
7181
|
+
}
|
|
7182
|
+
function normalizeRegistrySkill(raw) {
|
|
7183
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
7184
|
+
const obj = raw;
|
|
7185
|
+
const source = typeof obj.source === "string" ? obj.source : "";
|
|
7186
|
+
const skillId = typeof obj.skillId === "string" ? obj.skillId : "";
|
|
7187
|
+
if (!source || !skillId) return null;
|
|
7188
|
+
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name : skillId;
|
|
7189
|
+
const installs = typeof obj.installs === "number" ? obj.installs : 0;
|
|
7190
|
+
const id = typeof obj.id === "string" && obj.id ? obj.id : `${source}/${skillId}`;
|
|
7191
|
+
return {
|
|
7192
|
+
id,
|
|
7193
|
+
source,
|
|
7194
|
+
skillId,
|
|
7195
|
+
name,
|
|
7196
|
+
installs,
|
|
7197
|
+
installCommand: `npx skills add ${source}@${skillId}`,
|
|
7198
|
+
url: `https://skills.sh/${source}/${skillId}`
|
|
7199
|
+
};
|
|
7200
|
+
}
|
|
7201
|
+
async function searchRegistrySkills(query, opts = {}) {
|
|
7202
|
+
const trimmed = assertSearchableQuery(query);
|
|
7203
|
+
const limit = clampLimit(opts.limit ?? MAX_SKILL_RESULTS);
|
|
7204
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
7205
|
+
const url = `${REGISTRY_SEARCH_ENDPOINT}?q=${encodeURIComponent(trimmed)}&limit=${limit}`;
|
|
7206
|
+
let res;
|
|
7207
|
+
try {
|
|
7208
|
+
res = await fetchImpl(url, {
|
|
7209
|
+
headers: { Accept: "application/json", "User-Agent": "ghfind/1.0" }
|
|
7210
|
+
});
|
|
7211
|
+
} catch (err) {
|
|
7212
|
+
throw new Error(
|
|
7213
|
+
`Could not reach skills.sh: ${err instanceof Error ? err.message : String(err)}`
|
|
7214
|
+
);
|
|
7215
|
+
}
|
|
7216
|
+
if (!res.ok) {
|
|
7217
|
+
throw new Error(
|
|
7218
|
+
`skills.sh returned HTTP ${res.status} for "${trimmed}". Retry, or search locally with --local.`
|
|
7219
|
+
);
|
|
7220
|
+
}
|
|
7221
|
+
let payload;
|
|
7222
|
+
try {
|
|
7223
|
+
payload = await res.json();
|
|
7224
|
+
} catch {
|
|
7225
|
+
throw new Error("skills.sh returned a response that was not JSON.");
|
|
7226
|
+
}
|
|
7227
|
+
const items = payload?.skills ?? [];
|
|
7228
|
+
const skills = items.map(normalizeRegistrySkill).filter((s) => s !== null).slice(0, limit);
|
|
7229
|
+
return { query: trimmed, source: "registry", count: skills.length, skills };
|
|
7230
|
+
}
|
|
7231
|
+
async function searchSkills(query, opts = {}) {
|
|
7232
|
+
if ((opts.source ?? "local") === "registry") {
|
|
7233
|
+
return searchRegistrySkills(query, opts);
|
|
7234
|
+
}
|
|
7235
|
+
return searchLocalSkills(query, opts);
|
|
7236
|
+
}
|
|
7237
|
+
function formatSkillSearchJson(result) {
|
|
7238
|
+
return JSON.stringify(result, null, 2);
|
|
7239
|
+
}
|
|
7240
|
+
function shortDescription(description, width = 100) {
|
|
7241
|
+
if (description.length <= width) return description;
|
|
7242
|
+
return `${description.slice(0, width - 1).trimEnd()}\u2026`;
|
|
7243
|
+
}
|
|
7244
|
+
function formatSkillSearchText(result) {
|
|
7245
|
+
if (result.count === 0) {
|
|
7246
|
+
return result.source === "local" ? `No installed skills match "${result.query}". Try --remote to search the skills.sh ecosystem.` : `No skills.sh entries match "${result.query}".`;
|
|
7247
|
+
}
|
|
7248
|
+
if (result.source === "local") {
|
|
7249
|
+
return result.skills.map(
|
|
7250
|
+
(s) => `${s.name} (${s.origin})
|
|
7251
|
+
${shortDescription(s.description)}`
|
|
7252
|
+
).join("\n");
|
|
7253
|
+
}
|
|
7254
|
+
return result.skills.map(
|
|
7255
|
+
(s) => `${s.name} ${s.installs.toLocaleString()} installs [${s.source}]
|
|
7256
|
+
${s.installCommand}`
|
|
7257
|
+
).join("\n");
|
|
7258
|
+
}
|
|
7259
|
+
function formatSkillSearchNames(result) {
|
|
7260
|
+
return result.skills.map((s) => s.name).join("\n");
|
|
7261
|
+
}
|
|
7262
|
+
|
|
6403
7263
|
// src/mcp-server.ts
|
|
6404
7264
|
var SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
6405
7265
|
var LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
@@ -6462,6 +7322,7 @@ function asMcpToolError(err) {
|
|
|
6462
7322
|
}
|
|
6463
7323
|
var TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
|
|
6464
7324
|
var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
|
|
7325
|
+
var SKILL_SOURCES = ["local", "registry"];
|
|
6465
7326
|
var TRENDING_SINCE = ["daily", "weekly", "monthly"];
|
|
6466
7327
|
var PKG_SORTS = ["best-match", "score", "downloads", "name"];
|
|
6467
7328
|
var TOOLS = [
|
|
@@ -6806,6 +7667,52 @@ Not found: ${missing.join(", ")}` : "");
|
|
|
6806
7667
|
data: { name: "ghfind-cli" }
|
|
6807
7668
|
};
|
|
6808
7669
|
}
|
|
7670
|
+
},
|
|
7671
|
+
{
|
|
7672
|
+
name: "ghfind_skill_search",
|
|
7673
|
+
title: "Search agent skills",
|
|
7674
|
+
description: "Search installed agent skills (SKILL.md files under the user's skill directories) or the public skills.sh ecosystem by keyword, and return matching skills with their descriptions. Read-only: it never installs anything - registry hits include the `npx skills add` command for the user to run deliberately. Use when the user asks to find a skill for a task, or asks what skills are available.",
|
|
7675
|
+
inputSchema: {
|
|
7676
|
+
type: "object",
|
|
7677
|
+
properties: {
|
|
7678
|
+
query: {
|
|
7679
|
+
type: "string",
|
|
7680
|
+
description: 'Keyword to match against skill names and descriptions, e.g. "changelog" or "testing" (2+ characters).'
|
|
7681
|
+
},
|
|
7682
|
+
source: {
|
|
7683
|
+
type: "string",
|
|
7684
|
+
description: "Where to search: 'local' for skills installed on this machine (default), 'registry' for the public skills.sh ecosystem.",
|
|
7685
|
+
enum: [...SKILL_SOURCES]
|
|
7686
|
+
},
|
|
7687
|
+
limit: {
|
|
7688
|
+
type: "number",
|
|
7689
|
+
description: "Max results, 1-100 (default 20).",
|
|
7690
|
+
minimum: 1,
|
|
7691
|
+
maximum: 100
|
|
7692
|
+
}
|
|
7693
|
+
},
|
|
7694
|
+
required: ["query"]
|
|
7695
|
+
},
|
|
7696
|
+
async run(args) {
|
|
7697
|
+
const source = coerceEnum(
|
|
7698
|
+
args.source,
|
|
7699
|
+
SKILL_SOURCES,
|
|
7700
|
+
"local"
|
|
7701
|
+
);
|
|
7702
|
+
const limit = coerceInt(args.limit, 20, 1, 100);
|
|
7703
|
+
const result = await searchSkills(
|
|
7704
|
+
String(args.query ?? ""),
|
|
7705
|
+
{ source, limit }
|
|
7706
|
+
);
|
|
7707
|
+
const where = result.source === "local" ? "installed skills on this machine" : "the public skills.sh ecosystem";
|
|
7708
|
+
const header = `${result.count} match(es) for "${result.query}" in ${where}:`;
|
|
7709
|
+
return {
|
|
7710
|
+
text: `${header}
|
|
7711
|
+
|
|
7712
|
+
${formatSkillSearchText(result)}`,
|
|
7713
|
+
data: { ...result }
|
|
7714
|
+
};
|
|
7715
|
+
}
|
|
6809
7716
|
}
|
|
6810
7717
|
];
|
|
6811
7718
|
function toolSummaries() {
|
|
@@ -6884,7 +7791,7 @@ async function processServerLine(line, state, config) {
|
|
|
6884
7791
|
title: "ghfind \u2014 GitHub search for agents",
|
|
6885
7792
|
version: getVersion()
|
|
6886
7793
|
},
|
|
6887
|
-
instructions: "ghfind gives agents GitHub search: repositories, trending, npm packages, org/user profiles, repo comparison and deep-dives. Call ghfind_skill for the full CLI guide. Unauthenticated requests are rate-limited to 60/hr; pass a token or set GITHUB_TOKEN for 5,000/hr."
|
|
7794
|
+
instructions: "ghfind gives agents GitHub search: repositories, trending, npm packages, org/user profiles, repo comparison and deep-dives. ghfind_skill_search finds agent skills, either installed on this machine or in the public skills.sh ecosystem. Call ghfind_skill for the full CLI guide. Unauthenticated requests are rate-limited to 60/hr; pass a token or set GITHUB_TOKEN for 5,000/hr."
|
|
6888
7795
|
})
|
|
6889
7796
|
);
|
|
6890
7797
|
return;
|
|
@@ -6988,7 +7895,7 @@ async function runMcpServer(options = {}) {
|
|
|
6988
7895
|
// src/cli.ts
|
|
6989
7896
|
process.env.GHFIND_CLI_RUN = "1";
|
|
6990
7897
|
var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
|
|
6991
|
-
var PKG_DIR3 =
|
|
7898
|
+
var PKG_DIR3 = join7(__dirname3, "..");
|
|
6992
7899
|
function parseArgs(args) {
|
|
6993
7900
|
const flags = {
|
|
6994
7901
|
query: "",
|
|
@@ -7012,7 +7919,16 @@ function parseArgs(args) {
|
|
|
7012
7919
|
registry: "npm",
|
|
7013
7920
|
compare: [],
|
|
7014
7921
|
doctor: false,
|
|
7015
|
-
mcp: false
|
|
7922
|
+
mcp: false,
|
|
7923
|
+
bookmarks: false,
|
|
7924
|
+
history: false,
|
|
7925
|
+
topics: false,
|
|
7926
|
+
saved: false,
|
|
7927
|
+
remote: false,
|
|
7928
|
+
local: false,
|
|
7929
|
+
copy: false,
|
|
7930
|
+
raw: false,
|
|
7931
|
+
limitExplicit: false
|
|
7016
7932
|
};
|
|
7017
7933
|
const queryParts = [];
|
|
7018
7934
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -7069,6 +7985,7 @@ function parseArgs(args) {
|
|
|
7069
7985
|
break;
|
|
7070
7986
|
case "--limit": {
|
|
7071
7987
|
const parsed = parseInt(args[++i], 10);
|
|
7988
|
+
flags.limitExplicit = true;
|
|
7072
7989
|
if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
7073
7990
|
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
7074
7991
|
flags.limit = 50;
|
|
@@ -7101,9 +8018,56 @@ function parseArgs(args) {
|
|
|
7101
8018
|
case "--completion":
|
|
7102
8019
|
flags.completion = args[++i];
|
|
7103
8020
|
break;
|
|
8021
|
+
case "--remote":
|
|
8022
|
+
flags.remote = true;
|
|
8023
|
+
break;
|
|
8024
|
+
case "--local":
|
|
8025
|
+
flags.local = true;
|
|
8026
|
+
break;
|
|
8027
|
+
case "--copy":
|
|
8028
|
+
flags.copy = true;
|
|
8029
|
+
break;
|
|
8030
|
+
case "--raw":
|
|
8031
|
+
flags.raw = true;
|
|
8032
|
+
break;
|
|
8033
|
+
case "--as":
|
|
8034
|
+
flags.sharedAs = args[++i];
|
|
8035
|
+
break;
|
|
7104
8036
|
case "mcp":
|
|
7105
8037
|
flags.mcp = true;
|
|
7106
8038
|
break;
|
|
8039
|
+
case "bookmarks":
|
|
8040
|
+
flags.bookmarks = true;
|
|
8041
|
+
break;
|
|
8042
|
+
case "history":
|
|
8043
|
+
flags.history = true;
|
|
8044
|
+
break;
|
|
8045
|
+
case "topics":
|
|
8046
|
+
flags.topics = true;
|
|
8047
|
+
break;
|
|
8048
|
+
case "saved":
|
|
8049
|
+
flags.saved = true;
|
|
8050
|
+
break;
|
|
8051
|
+
case "readme": {
|
|
8052
|
+
const next = args[i + 1];
|
|
8053
|
+
if (next && !next.startsWith("-")) {
|
|
8054
|
+
flags.readme = next;
|
|
8055
|
+
i++;
|
|
8056
|
+
} else {
|
|
8057
|
+
flags.readme = "";
|
|
8058
|
+
}
|
|
8059
|
+
break;
|
|
8060
|
+
}
|
|
8061
|
+
case "share": {
|
|
8062
|
+
const next = args[i + 1];
|
|
8063
|
+
if (next && !next.startsWith("-")) {
|
|
8064
|
+
flags.share = next;
|
|
8065
|
+
i++;
|
|
8066
|
+
} else {
|
|
8067
|
+
flags.share = "";
|
|
8068
|
+
}
|
|
8069
|
+
break;
|
|
8070
|
+
}
|
|
7107
8071
|
case "deep-dive": {
|
|
7108
8072
|
const next = args[i + 1];
|
|
7109
8073
|
if (next && !next.startsWith("-")) {
|
|
@@ -7114,6 +8078,17 @@ function parseArgs(args) {
|
|
|
7114
8078
|
}
|
|
7115
8079
|
case "skill": {
|
|
7116
8080
|
const next = args[i + 1];
|
|
8081
|
+
if (next === "search") {
|
|
8082
|
+
i++;
|
|
8083
|
+
const query = args[i + 1];
|
|
8084
|
+
if (query && !query.startsWith("-")) {
|
|
8085
|
+
flags.skillSearch = query;
|
|
8086
|
+
i++;
|
|
8087
|
+
} else {
|
|
8088
|
+
flags.skillSearch = "";
|
|
8089
|
+
}
|
|
8090
|
+
break;
|
|
8091
|
+
}
|
|
7117
8092
|
if (next && !next.startsWith("-")) {
|
|
7118
8093
|
flags.skill = next;
|
|
7119
8094
|
i++;
|
|
@@ -7192,6 +8167,13 @@ Usage:
|
|
|
7192
8167
|
ghfind pkg <query> Search npm packages, text list
|
|
7193
8168
|
ghfind mcp Run the MCP server for AI agents (stdio)
|
|
7194
8169
|
ghfind skill [name] Print the agent skill guide (or catalog)
|
|
8170
|
+
ghfind bookmarks [query] Saved repos, newest first
|
|
8171
|
+
ghfind history [query] Past searches, newest first
|
|
8172
|
+
ghfind topics Browse popular GitHub topics
|
|
8173
|
+
ghfind saved Named searches saved in the TUI
|
|
8174
|
+
ghfind readme <owner/repo> Print a repository README
|
|
8175
|
+
ghfind share a/b --as gh-cli Share snippet (--copy to clipboard)
|
|
8176
|
+
ghfind skill search <query> Find skills locally, or --remote
|
|
7195
8177
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
7196
8178
|
ghfind init Run setup wizard
|
|
7197
8179
|
ghfind login Import the gh CLI token or paste one
|
|
@@ -7216,14 +8198,19 @@ Options:
|
|
|
7216
8198
|
--releases New-release feed for bookmarks
|
|
7217
8199
|
--interval <s> Watch interval in seconds (default: 300)
|
|
7218
8200
|
--completion <shell> Generate completions
|
|
8201
|
+
--remote skill search: query skills.sh instead of this machine
|
|
8202
|
+
--local skill search: force the local scan (default)
|
|
8203
|
+
--as <fmt> share format: markdown|plain|gh-cli|short
|
|
8204
|
+
--copy share: also copy the snippet to the clipboard
|
|
8205
|
+
--raw readme: print the raw markdown with no header
|
|
7219
8206
|
`);
|
|
7220
8207
|
return;
|
|
7221
8208
|
}
|
|
7222
8209
|
if (flags.completion) {
|
|
7223
8210
|
const shell = flags.completion;
|
|
7224
8211
|
try {
|
|
7225
|
-
const content =
|
|
7226
|
-
|
|
8212
|
+
const content = readFileSync7(
|
|
8213
|
+
join7(PKG_DIR3, "completions", `ghfind.${shell}`),
|
|
7227
8214
|
"utf-8"
|
|
7228
8215
|
);
|
|
7229
8216
|
console.log(content);
|
|
@@ -7245,10 +8232,38 @@ Options:
|
|
|
7245
8232
|
await runMcpServer({ token: flags.token });
|
|
7246
8233
|
return;
|
|
7247
8234
|
}
|
|
8235
|
+
if (flags.skillSearch !== void 0) {
|
|
8236
|
+
await runSkillSearch(flags);
|
|
8237
|
+
return;
|
|
8238
|
+
}
|
|
7248
8239
|
if (flags.skill !== void 0) {
|
|
7249
8240
|
printAgentSkill(flags.skill);
|
|
7250
8241
|
return;
|
|
7251
8242
|
}
|
|
8243
|
+
if (flags.bookmarks) {
|
|
8244
|
+
runBookmarks(flags);
|
|
8245
|
+
return;
|
|
8246
|
+
}
|
|
8247
|
+
if (flags.history) {
|
|
8248
|
+
runHistory(flags);
|
|
8249
|
+
return;
|
|
8250
|
+
}
|
|
8251
|
+
if (flags.saved) {
|
|
8252
|
+
runSavedSearches(flags);
|
|
8253
|
+
return;
|
|
8254
|
+
}
|
|
8255
|
+
if (flags.topics) {
|
|
8256
|
+
await runTopics(flags);
|
|
8257
|
+
return;
|
|
8258
|
+
}
|
|
8259
|
+
if (flags.readme !== void 0) {
|
|
8260
|
+
await runReadme(flags);
|
|
8261
|
+
return;
|
|
8262
|
+
}
|
|
8263
|
+
if (flags.share !== void 0) {
|
|
8264
|
+
await runShare(flags);
|
|
8265
|
+
return;
|
|
8266
|
+
}
|
|
7252
8267
|
if (flags.pkg) {
|
|
7253
8268
|
const interactive = !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
|
|
7254
8269
|
if (interactive) {
|
|
@@ -7382,6 +8397,233 @@ async function runDeepDive(flags, outputFormat) {
|
|
|
7382
8397
|
process.exit(1);
|
|
7383
8398
|
}
|
|
7384
8399
|
}
|
|
8400
|
+
async function runSkillSearch(flags) {
|
|
8401
|
+
const query = flags.skillSearch ?? "";
|
|
8402
|
+
if (!query.trim()) {
|
|
8403
|
+
console.error(
|
|
8404
|
+
"Usage: ghfind skill search <query> [--remote] [--limit <n>] [--json]"
|
|
8405
|
+
);
|
|
8406
|
+
process.exit(1);
|
|
8407
|
+
}
|
|
8408
|
+
if (flags.remote && flags.local) {
|
|
8409
|
+
console.error(
|
|
8410
|
+
"Contradictory flags: pass either --remote or --local, not both."
|
|
8411
|
+
);
|
|
8412
|
+
process.exit(1);
|
|
8413
|
+
}
|
|
8414
|
+
const source = flags.remote ? "registry" : "local";
|
|
8415
|
+
try {
|
|
8416
|
+
const result = await searchSkills(query, {
|
|
8417
|
+
source,
|
|
8418
|
+
limit: flags.limit
|
|
8419
|
+
});
|
|
8420
|
+
if (flags.json) {
|
|
8421
|
+
console.log(formatSkillSearchJson(result));
|
|
8422
|
+
} else if (flags.format === "names") {
|
|
8423
|
+
console.log(formatSkillSearchNames(result));
|
|
8424
|
+
} else {
|
|
8425
|
+
console.log(formatSkillSearchText(result));
|
|
8426
|
+
}
|
|
8427
|
+
} catch (err) {
|
|
8428
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8429
|
+
process.exit(1);
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8432
|
+
function runBookmarks(flags) {
|
|
8433
|
+
const query = flags.query.trim();
|
|
8434
|
+
const bookmarks = query ? searchBookmarks(query) : getBookmarks();
|
|
8435
|
+
if (flags.json) {
|
|
8436
|
+
console.log(JSON.stringify(bookmarks, null, 2));
|
|
8437
|
+
return;
|
|
8438
|
+
}
|
|
8439
|
+
if (bookmarks.length === 0) {
|
|
8440
|
+
console.log(
|
|
8441
|
+
query ? `No bookmarks match "${query}".` : "No bookmarks yet. Press Space \u2192 Bookmark in the TUI to save one."
|
|
8442
|
+
);
|
|
8443
|
+
return;
|
|
8444
|
+
}
|
|
8445
|
+
if (flags.csv || flags.markdown) {
|
|
8446
|
+
const header = ["full_name", "stars", "language", "tags", "saved_at"];
|
|
8447
|
+
const rows = bookmarks.map((b) => [
|
|
8448
|
+
b.repo.fullName,
|
|
8449
|
+
String(b.repo.stars),
|
|
8450
|
+
b.repo.language ?? "",
|
|
8451
|
+
b.tags.join(" "),
|
|
8452
|
+
new Date(b.savedAt).toISOString().slice(0, 10)
|
|
8453
|
+
]);
|
|
8454
|
+
console.log(
|
|
8455
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows)
|
|
8456
|
+
);
|
|
8457
|
+
return;
|
|
8458
|
+
}
|
|
8459
|
+
for (const b of bookmarks) {
|
|
8460
|
+
const tags = b.tags.length > 0 ? ` [${b.tags.join(", ")}]` : "";
|
|
8461
|
+
console.log(
|
|
8462
|
+
`${b.repo.fullName} \u2014 ${b.repo.stars.toLocaleString()} ${b.repo.language ?? "n/a"}${tags}`
|
|
8463
|
+
);
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
function runHistory(flags) {
|
|
8467
|
+
const limit = flags.limitExplicit ? flags.limit : 20;
|
|
8468
|
+
const query = flags.query.trim().toLowerCase();
|
|
8469
|
+
const entries = readHistory().filter((e) => !query || e.query.toLowerCase().includes(query)).slice(0, limit);
|
|
8470
|
+
if (flags.json) {
|
|
8471
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
8472
|
+
return;
|
|
8473
|
+
}
|
|
8474
|
+
if (entries.length === 0) {
|
|
8475
|
+
console.log(
|
|
8476
|
+
query ? `No history matches "${query}".` : "No search history yet."
|
|
8477
|
+
);
|
|
8478
|
+
return;
|
|
8479
|
+
}
|
|
8480
|
+
if (flags.csv || flags.markdown) {
|
|
8481
|
+
const header = ["query", "mode", "results", "when"];
|
|
8482
|
+
const rows = entries.map((e) => [
|
|
8483
|
+
e.query,
|
|
8484
|
+
e.mode,
|
|
8485
|
+
String(e.resultCount),
|
|
8486
|
+
new Date(e.timestamp).toISOString()
|
|
8487
|
+
]);
|
|
8488
|
+
console.log(
|
|
8489
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows)
|
|
8490
|
+
);
|
|
8491
|
+
return;
|
|
8492
|
+
}
|
|
8493
|
+
for (const e of entries) {
|
|
8494
|
+
const when = new Date(e.timestamp).toISOString().slice(0, 10);
|
|
8495
|
+
console.log(
|
|
8496
|
+
`${e.query} \u2014 ${e.mode} \u2014 ${e.resultCount} results \u2014 ${when}`
|
|
8497
|
+
);
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8500
|
+
function runSavedSearches(flags) {
|
|
8501
|
+
const saved = getSavedSearches();
|
|
8502
|
+
if (flags.json) {
|
|
8503
|
+
console.log(JSON.stringify(saved, null, 2));
|
|
8504
|
+
return;
|
|
8505
|
+
}
|
|
8506
|
+
if (saved.length === 0) {
|
|
8507
|
+
console.log("No saved searches yet. Save one from the TUI command menu.");
|
|
8508
|
+
return;
|
|
8509
|
+
}
|
|
8510
|
+
if (flags.csv || flags.markdown) {
|
|
8511
|
+
const header = ["name", "query", "mode", "limit"];
|
|
8512
|
+
const rows = saved.map((s) => [s.name, s.query, s.mode, String(s.limit)]);
|
|
8513
|
+
console.log(
|
|
8514
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows)
|
|
8515
|
+
);
|
|
8516
|
+
return;
|
|
8517
|
+
}
|
|
8518
|
+
for (const s of saved) {
|
|
8519
|
+
console.log(`${s.name} \u2014 ${s.query} \u2014 ${s.mode} \u2014 limit ${s.limit}`);
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
function rowsToCsv(header, rows) {
|
|
8523
|
+
const quote = (value) => /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
8524
|
+
return [header, ...rows].map((row) => row.map(quote).join(",")).join("\n");
|
|
8525
|
+
}
|
|
8526
|
+
function rowsToMarkdown(header, rows) {
|
|
8527
|
+
return [
|
|
8528
|
+
`| ${header.join(" | ")} |`,
|
|
8529
|
+
`|${header.map(() => "---").join("|")}|`,
|
|
8530
|
+
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
8531
|
+
].join("\n");
|
|
8532
|
+
}
|
|
8533
|
+
function truncate2(text, width) {
|
|
8534
|
+
if (text.length <= width) return text;
|
|
8535
|
+
return `${text.slice(0, width - 1).trimEnd()}\u2026`;
|
|
8536
|
+
}
|
|
8537
|
+
function plainText(html) {
|
|
8538
|
+
return html.replace(/<\/p>|<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
8539
|
+
}
|
|
8540
|
+
async function runTopics(flags) {
|
|
8541
|
+
try {
|
|
8542
|
+
const topics = (await fetchTopics()).slice(0, flags.limit);
|
|
8543
|
+
if (flags.json) {
|
|
8544
|
+
console.log(JSON.stringify(topics, null, 2));
|
|
8545
|
+
return;
|
|
8546
|
+
}
|
|
8547
|
+
if (topics.length === 0) {
|
|
8548
|
+
console.log("No topics returned.");
|
|
8549
|
+
return;
|
|
8550
|
+
}
|
|
8551
|
+
for (const t3 of topics) {
|
|
8552
|
+
const description = truncate2(plainText(t3.description), 140);
|
|
8553
|
+
console.log(description ? `${t3.name} ${description}` : t3.name);
|
|
8554
|
+
}
|
|
8555
|
+
} catch (err) {
|
|
8556
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8557
|
+
process.exit(1);
|
|
8558
|
+
}
|
|
8559
|
+
}
|
|
8560
|
+
async function runReadme(flags) {
|
|
8561
|
+
const ref = flags.readme ?? "";
|
|
8562
|
+
if (!ref) {
|
|
8563
|
+
console.error(
|
|
8564
|
+
"Usage: ghfind readme <owner/repo> [--json] [--raw] [--token <t>]"
|
|
8565
|
+
);
|
|
8566
|
+
process.exit(1);
|
|
8567
|
+
}
|
|
8568
|
+
try {
|
|
8569
|
+
const { owner, name } = repoFromRef(ref);
|
|
8570
|
+
const readme = await fetchReadme(owner, name, { token: flags.token });
|
|
8571
|
+
if (!readme) {
|
|
8572
|
+
console.error(`No README found for ${owner}/${name}.`);
|
|
8573
|
+
process.exit(1);
|
|
8574
|
+
}
|
|
8575
|
+
if (flags.json) {
|
|
8576
|
+
console.log(
|
|
8577
|
+
JSON.stringify(
|
|
8578
|
+
{ owner, name, sourceUrl: readme.sourceUrl, text: readme.text },
|
|
8579
|
+
null,
|
|
8580
|
+
2
|
|
8581
|
+
)
|
|
8582
|
+
);
|
|
8583
|
+
return;
|
|
8584
|
+
}
|
|
8585
|
+
if (!flags.raw) {
|
|
8586
|
+
console.log(`# ${owner}/${name} README (${readme.sourceUrl})`);
|
|
8587
|
+
console.log("");
|
|
8588
|
+
}
|
|
8589
|
+
console.log(readme.text);
|
|
8590
|
+
} catch (err) {
|
|
8591
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8592
|
+
process.exit(1);
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
async function runShare(flags) {
|
|
8596
|
+
const ref = flags.share ?? "";
|
|
8597
|
+
if (!ref) {
|
|
8598
|
+
console.error(
|
|
8599
|
+
"Usage: ghfind share <owner/repo> [--as markdown|plain|gh-cli|short] [--copy]"
|
|
8600
|
+
);
|
|
8601
|
+
process.exit(1);
|
|
8602
|
+
}
|
|
8603
|
+
const SHARE_FORMATS = ["markdown", "plain", "gh-cli", "short"];
|
|
8604
|
+
const as = flags.sharedAs ?? "markdown";
|
|
8605
|
+
if (!SHARE_FORMATS.includes(as)) {
|
|
8606
|
+
console.error(
|
|
8607
|
+
`Invalid --as value: "${flags.sharedAs}" (use markdown|plain|gh-cli|short).`
|
|
8608
|
+
);
|
|
8609
|
+
process.exit(1);
|
|
8610
|
+
}
|
|
8611
|
+
try {
|
|
8612
|
+
repoFromRef(ref);
|
|
8613
|
+
const repo = await resolveRepoFromRef(ref, flags.token);
|
|
8614
|
+
const text = formatShare(repo, as);
|
|
8615
|
+
console.log(text);
|
|
8616
|
+
if (flags.copy) {
|
|
8617
|
+
const copied = await copyToClipboard(text);
|
|
8618
|
+
console.error(
|
|
8619
|
+
copied ? "Copied to clipboard." : "Could not copy to the clipboard on this platform."
|
|
8620
|
+
);
|
|
8621
|
+
}
|
|
8622
|
+
} catch (err) {
|
|
8623
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8624
|
+
process.exit(1);
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
7385
8627
|
async function runPackageSearch(flags) {
|
|
7386
8628
|
if (flags.registry !== "npm") {
|
|
7387
8629
|
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|