skilld 1.7.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/add.mjs +66 -0
- package/dist/_chunks/add.mjs.map +1 -0
- package/dist/_chunks/agent-prompt.mjs +88 -0
- package/dist/_chunks/agent-prompt.mjs.map +1 -0
- package/dist/_chunks/agent.mjs +737 -619
- package/dist/_chunks/agent.mjs.map +1 -1
- package/dist/_chunks/args.mjs +42 -0
- package/dist/_chunks/args.mjs.map +1 -0
- package/dist/_chunks/assemble.mjs +11 -8
- package/dist/_chunks/assemble.mjs.map +1 -1
- package/dist/_chunks/author.mjs +77 -131
- package/dist/_chunks/author.mjs.map +1 -1
- package/dist/_chunks/cache.mjs +320 -54
- package/dist/_chunks/cache.mjs.map +1 -1
- package/dist/_chunks/cache2.mjs +7 -6
- package/dist/_chunks/cache2.mjs.map +1 -1
- package/dist/_chunks/client.mjs +117 -0
- package/dist/_chunks/client.mjs.map +1 -0
- package/dist/_chunks/core.mjs +7 -4
- package/dist/_chunks/detect.mjs +54 -44
- package/dist/_chunks/detect.mjs.map +1 -1
- package/dist/_chunks/eject.mjs +69 -0
- package/dist/_chunks/eject.mjs.map +1 -0
- package/dist/_chunks/embedding-cache2.mjs +2 -2
- package/dist/_chunks/env.mjs +19 -0
- package/dist/_chunks/env.mjs.map +1 -0
- package/dist/_chunks/install-many.mjs +376 -0
- package/dist/_chunks/install-many.mjs.map +1 -0
- package/dist/_chunks/install.mjs +86 -371
- package/dist/_chunks/install.mjs.map +1 -1
- package/dist/_chunks/intro.mjs +63 -0
- package/dist/_chunks/intro.mjs.map +1 -0
- package/dist/_chunks/list.mjs +2 -2
- package/dist/_chunks/list.mjs.map +1 -1
- package/dist/_chunks/lockfile.mjs +31 -7
- package/dist/_chunks/lockfile.mjs.map +1 -1
- package/dist/_chunks/login.mjs +233 -0
- package/dist/_chunks/login.mjs.map +1 -0
- package/dist/_chunks/logout.mjs +27 -0
- package/dist/_chunks/logout.mjs.map +1 -0
- package/dist/_chunks/map.mjs +11 -0
- package/dist/_chunks/map.mjs.map +1 -0
- package/dist/_chunks/markdown.mjs +79 -54
- package/dist/_chunks/markdown.mjs.map +1 -1
- package/dist/_chunks/menu.mjs +33 -0
- package/dist/_chunks/menu.mjs.map +1 -0
- package/dist/_chunks/model-picker.mjs +61 -0
- package/dist/_chunks/model-picker.mjs.map +1 -0
- package/dist/_chunks/monorepo.mjs +73 -0
- package/dist/_chunks/monorepo.mjs.map +1 -0
- package/dist/_chunks/package-json.mjs.map +1 -1
- package/dist/_chunks/paths.mjs +47 -0
- package/dist/_chunks/paths.mjs.map +1 -0
- package/dist/_chunks/pipeline.mjs +985 -0
- package/dist/_chunks/pipeline.mjs.map +1 -0
- package/dist/_chunks/pool2.mjs +2 -2
- package/dist/_chunks/portable.mjs +151 -0
- package/dist/_chunks/portable.mjs.map +1 -0
- package/dist/_chunks/prepare-hook.mjs +2 -0
- package/dist/_chunks/prepare-hook2.mjs +61 -0
- package/dist/_chunks/prepare-hook2.mjs.map +1 -0
- package/dist/_chunks/prepare.mjs +47 -3
- package/dist/_chunks/prepare.mjs.map +1 -1
- package/dist/_chunks/prepare2.mjs +9 -8
- package/dist/_chunks/prepare2.mjs.map +1 -1
- package/dist/_chunks/prompts.mjs +784 -26
- package/dist/_chunks/prompts.mjs.map +1 -1
- package/dist/_chunks/pull.mjs +219 -0
- package/dist/_chunks/pull.mjs.map +1 -0
- package/dist/_chunks/regex.mjs +19 -0
- package/dist/_chunks/regex.mjs.map +1 -0
- package/dist/_chunks/retriv.mjs +2 -171
- package/dist/_chunks/retriv2.mjs +159 -0
- package/dist/_chunks/retriv2.mjs.map +1 -0
- package/dist/_chunks/sanitize.mjs +12 -9
- package/dist/_chunks/sanitize.mjs.map +1 -1
- package/dist/_chunks/search-helpers.mjs +9 -8
- package/dist/_chunks/search-helpers.mjs.map +1 -1
- package/dist/_chunks/search-interactive.mjs +23 -20
- package/dist/_chunks/search-interactive.mjs.map +1 -1
- package/dist/_chunks/search.mjs +3 -4
- package/dist/_chunks/search.mjs.map +1 -1
- package/dist/_chunks/{sources.mjs → semver.mjs} +1128 -838
- package/dist/_chunks/semver.mjs.map +1 -0
- package/dist/_chunks/skill-installer.mjs +2 -0
- package/dist/_chunks/skill-installer2.mjs +154 -0
- package/dist/_chunks/skill-installer2.mjs.map +1 -0
- package/dist/_chunks/skills.mjs +12 -12
- package/dist/_chunks/skills.mjs.map +1 -1
- package/dist/_chunks/store.mjs +107 -0
- package/dist/_chunks/store.mjs.map +1 -0
- package/dist/_chunks/sync.mjs +761 -1349
- package/dist/_chunks/sync.mjs.map +1 -1
- package/dist/_chunks/sync2.mjs +2 -3
- package/dist/_chunks/telemetry.mjs +26 -0
- package/dist/_chunks/telemetry.mjs.map +1 -0
- package/dist/_chunks/uninstall.mjs +15 -13
- package/dist/_chunks/uninstall.mjs.map +1 -1
- package/dist/_chunks/update.mjs +171 -0
- package/dist/_chunks/update.mjs.map +1 -0
- package/dist/_chunks/upload.mjs +4 -4
- package/dist/_chunks/validate.mjs +1 -1
- package/dist/_chunks/version.mjs +16 -27
- package/dist/_chunks/version.mjs.map +1 -1
- package/dist/_chunks/whoami.mjs +21 -0
- package/dist/_chunks/whoami.mjs.map +1 -0
- package/dist/_chunks/wizard.mjs +2 -190
- package/dist/_chunks/wizard2.mjs +200 -0
- package/dist/_chunks/wizard2.mjs.map +1 -0
- package/dist/cli.mjs +77 -59
- package/dist/cli.mjs.map +1 -1
- package/dist/prepare.mjs +5 -4
- package/dist/prepare.mjs.map +1 -1
- package/dist/retriv/worker.d.mts +5 -1
- package/dist/retriv/worker.d.mts.map +1 -1
- package/dist/retriv/worker.mjs +1 -1
- package/package.json +20 -29
- package/dist/_chunks/author-group.mjs +0 -17
- package/dist/_chunks/author-group.mjs.map +0 -1
- package/dist/_chunks/cli-helpers.mjs +0 -335
- package/dist/_chunks/cli-helpers.mjs.map +0 -1
- package/dist/_chunks/cli-helpers2.mjs +0 -2
- package/dist/_chunks/config.mjs +0 -122
- package/dist/_chunks/config.mjs.map +0 -1
- package/dist/_chunks/index.d.mts +0 -151
- package/dist/_chunks/index.d.mts.map +0 -1
- package/dist/_chunks/index2.d.mts +0 -44
- package/dist/_chunks/index2.d.mts.map +0 -1
- package/dist/_chunks/index3.d.mts +0 -589
- package/dist/_chunks/index3.d.mts.map +0 -1
- package/dist/_chunks/prefix.mjs +0 -108
- package/dist/_chunks/prefix.mjs.map +0 -1
- package/dist/_chunks/retriv.mjs.map +0 -1
- package/dist/_chunks/setup.mjs +0 -17
- package/dist/_chunks/setup.mjs.map +0 -1
- package/dist/_chunks/shared.mjs +0 -503
- package/dist/_chunks/shared.mjs.map +0 -1
- package/dist/_chunks/skill.mjs +0 -329
- package/dist/_chunks/skill.mjs.map +0 -1
- package/dist/_chunks/sources.mjs.map +0 -1
- package/dist/_chunks/sync-registry.mjs +0 -59
- package/dist/_chunks/sync-registry.mjs.map +0 -1
- package/dist/_chunks/sync-shared.mjs +0 -2
- package/dist/_chunks/sync-shared2.mjs +0 -1020
- package/dist/_chunks/sync-shared2.mjs.map +0 -1
- package/dist/_chunks/types.d.mts +0 -88
- package/dist/_chunks/types.d.mts.map +0 -1
- package/dist/_chunks/wizard.mjs.map +0 -1
- package/dist/agent/index.d.mts +0 -346
- package/dist/agent/index.d.mts.map +0 -1
- package/dist/agent/index.mjs +0 -5
- package/dist/cache/index.d.mts +0 -2
- package/dist/cache/index.mjs +0 -4
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -5
- package/dist/retriv/index.d.mts +0 -3
- package/dist/retriv/index.mjs +0 -2
- package/dist/sources/index.d.mts +0 -2
- package/dist/sources/index.mjs +0 -3
- package/dist/types.d.mts +0 -4
- package/dist/types.mjs +0 -1
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as
|
|
1
|
+
import { D as getCrawlUrl, E as getBlogPreset, O as getDocOverride } from "./prompts.mjs";
|
|
2
|
+
import { a as GIT_PROTOCOL_PREFIX_RE, c as NPM_SCOPE_PREFIX_RE, g as V_PREFIX_RE, h as VERSION_RANGE_PREFIX_RE, i as GIT_PLUS_PREFIX_RE, l as NPM_SCOPE_WITH_SLASH_RE, m as TRAILING_SLASH_RE, o as GIT_SUFFIX_RE, r as GITHUB_SSH_URL_PREFIX_RE, s as LEADING_SLASH_RE, u as README_FILENAME_RE } from "./regex.mjs";
|
|
3
3
|
import { t as yamlEscape } from "./yaml.mjs";
|
|
4
|
+
import { s as getCacheDir } from "./prepare.mjs";
|
|
5
|
+
import { i as readPackageJsonSafe } from "./package-json.mjs";
|
|
6
|
+
import "./cache.mjs";
|
|
7
|
+
import { t as mapInsert } from "./map.mjs";
|
|
4
8
|
import { i as parseFrontmatter, n as extractLinks, r as extractTitle, t as extractDescription } from "./markdown.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { basename, dirname, join, resolve } from "pathe";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
8
10
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
9
|
-
import { htmlToMarkdown } from "mdream";
|
|
10
11
|
import pLimit from "p-limit";
|
|
12
|
+
import { basename, dirname, join, resolve } from "pathe";
|
|
11
13
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import {
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { glob } from "node:fs/promises";
|
|
13
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
import { htmlToMarkdown } from "mdream";
|
|
18
|
+
import { ofetch } from "ofetch";
|
|
14
19
|
import { crawlAndGenerate } from "@mdream/crawl";
|
|
15
|
-
import { glob } from "tinyglobby";
|
|
16
20
|
import { downloadTemplate } from "giget";
|
|
17
21
|
import { Writable } from "node:stream";
|
|
18
|
-
import {
|
|
22
|
+
import { diff, gt, valid } from "semver";
|
|
23
|
+
const STATIC_REGEX_1$9 = /```[\s\S]*?```/;
|
|
24
|
+
const STATIC_REGEX_2$4 = /`[^`]+`/;
|
|
19
25
|
const BOT_USERS = new Set([
|
|
20
26
|
"renovate[bot]",
|
|
21
27
|
"dependabot[bot]",
|
|
@@ -31,7 +37,7 @@ function buildFrontmatter(fields) {
|
|
|
31
37
|
return lines.join("\n");
|
|
32
38
|
}
|
|
33
39
|
function hasCodeBlock(text) {
|
|
34
|
-
return
|
|
40
|
+
return STATIC_REGEX_1$9.test(text) || STATIC_REGEX_2$4.test(text);
|
|
35
41
|
}
|
|
36
42
|
const COMMENT_NOISE_RE = /^(?:\+1|👍|same here|any update|bump|following|is there any progress|when will this|me too|i have the same|same issue|thanks|thank you)[\s!?.]*$/i;
|
|
37
43
|
function truncateBody(body, limit) {
|
|
@@ -79,6 +85,15 @@ function markRepoPrivate(owner, repo) {
|
|
|
79
85
|
function isKnownPrivateRepo(owner, repo) {
|
|
80
86
|
return _needsAuth.has(`${owner}/${repo}`);
|
|
81
87
|
}
|
|
88
|
+
async function fetchUnghOrApi(owner, repo, ungh, api) {
|
|
89
|
+
if (!isKnownPrivateRepo(owner, repo)) {
|
|
90
|
+
const r = await ungh().catch(() => null);
|
|
91
|
+
if (r) return r;
|
|
92
|
+
}
|
|
93
|
+
const r = await api();
|
|
94
|
+
if (r) markRepoPrivate(owner, repo);
|
|
95
|
+
return r;
|
|
96
|
+
}
|
|
82
97
|
const GH_API = "https://api.github.com";
|
|
83
98
|
const ghApiFetch = ofetch.create({
|
|
84
99
|
retry: 2,
|
|
@@ -172,92 +187,12 @@ async function verifyUrl(url) {
|
|
|
172
187
|
if (!res) return false;
|
|
173
188
|
return !(res.headers.get("content-type") || "").includes("text/html");
|
|
174
189
|
}
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"facebook.com",
|
|
179
|
-
"linkedin.com",
|
|
180
|
-
"youtube.com",
|
|
181
|
-
"instagram.com",
|
|
182
|
-
"npmjs.com",
|
|
183
|
-
"www.npmjs.com",
|
|
184
|
-
"yarnpkg.com"
|
|
185
|
-
]);
|
|
186
|
-
function isUselessDocsUrl(url) {
|
|
187
|
-
try {
|
|
188
|
-
const { hostname } = new URL(url);
|
|
189
|
-
return USELESS_HOSTS.has(hostname);
|
|
190
|
-
} catch {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function isGitHubRepoUrl(url) {
|
|
195
|
-
try {
|
|
196
|
-
const parsed = new URL(url);
|
|
197
|
-
return parsed.hostname === "github.com" || parsed.hostname === "www.github.com";
|
|
198
|
-
} catch {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
function isLikelyCodeHostUrl(url) {
|
|
203
|
-
if (!url) return false;
|
|
204
|
-
try {
|
|
205
|
-
const parsed = new URL(url);
|
|
206
|
-
return [
|
|
207
|
-
"github.com",
|
|
208
|
-
"www.github.com",
|
|
209
|
-
"gitlab.com",
|
|
210
|
-
"www.gitlab.com"
|
|
211
|
-
].includes(parsed.hostname);
|
|
212
|
-
} catch {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
function parseGitHubUrl(url) {
|
|
217
|
-
const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[/#]|$)/);
|
|
218
|
-
if (!match) return null;
|
|
219
|
-
return {
|
|
220
|
-
owner: match[1],
|
|
221
|
-
repo: match[2]
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
function parseGitHubRepoSlug(url) {
|
|
225
|
-
if (!url) return void 0;
|
|
226
|
-
const parsed = parseGitHubUrl(url);
|
|
227
|
-
return parsed ? `${parsed.owner}/${parsed.repo}` : void 0;
|
|
228
|
-
}
|
|
229
|
-
function normalizeRepoUrl(url) {
|
|
230
|
-
return url.replace(/^git\+/, "").replace(/#.*$/, "").replace(/\.git$/, "").replace(/^git:\/\//, "https://").replace(/^ssh:\/\/git@github\.com/, "https://github.com").replace(/^git@github\.com:/, "https://github.com/");
|
|
231
|
-
}
|
|
232
|
-
function parsePackageSpec(spec) {
|
|
233
|
-
if (spec.startsWith("@")) {
|
|
234
|
-
const slashIdx = spec.indexOf("/");
|
|
235
|
-
if (slashIdx !== -1) {
|
|
236
|
-
const atIdx = spec.indexOf("@", slashIdx + 1);
|
|
237
|
-
if (atIdx !== -1) return {
|
|
238
|
-
name: spec.slice(0, atIdx),
|
|
239
|
-
tag: spec.slice(atIdx + 1)
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
return { name: spec };
|
|
243
|
-
}
|
|
244
|
-
const atIdx = spec.indexOf("@");
|
|
245
|
-
if (atIdx !== -1) return {
|
|
246
|
-
name: spec.slice(0, atIdx),
|
|
247
|
-
tag: spec.slice(atIdx + 1)
|
|
248
|
-
};
|
|
249
|
-
return { name: spec };
|
|
250
|
-
}
|
|
251
|
-
function extractBranchHint(url) {
|
|
252
|
-
const hash = url.indexOf("#");
|
|
253
|
-
if (hash === -1) return void 0;
|
|
254
|
-
const fragment = url.slice(hash + 1);
|
|
255
|
-
if (!fragment || fragment === "readme") return void 0;
|
|
256
|
-
return fragment;
|
|
257
|
-
}
|
|
190
|
+
const STATIC_REGEX_2$3 = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
|
|
191
|
+
const STATIC_REGEX_3$3 = /^\d+\.\d+\.\d+-.+/;
|
|
192
|
+
const STATIC_REGEX_4$1 = /changelog\.md/i;
|
|
258
193
|
function parseSemver(version) {
|
|
259
|
-
const clean = version.replace(
|
|
260
|
-
const match = clean.match(
|
|
194
|
+
const clean = version.replace(V_PREFIX_RE, "");
|
|
195
|
+
const match = clean.match(STATIC_REGEX_2$3);
|
|
261
196
|
if (!match) return null;
|
|
262
197
|
return {
|
|
263
198
|
major: +match[1],
|
|
@@ -273,7 +208,7 @@ function extractVersion(tag, packageName) {
|
|
|
273
208
|
const dashMatch = tag.match(new RegExp(`^${escapeRegex(packageName)}-v?(.+)$`));
|
|
274
209
|
if (dashMatch) return dashMatch[1];
|
|
275
210
|
}
|
|
276
|
-
return tag.replace(
|
|
211
|
+
return tag.replace(V_PREFIX_RE, "");
|
|
277
212
|
}
|
|
278
213
|
function escapeRegex(str) {
|
|
279
214
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -282,7 +217,7 @@ function tagMatchesPackage(tag, packageName) {
|
|
|
282
217
|
return tag.startsWith(`${packageName}@`) || tag.startsWith(`${packageName}-v`) || tag.startsWith(`${packageName}-`);
|
|
283
218
|
}
|
|
284
219
|
function isPrerelease(version) {
|
|
285
|
-
return
|
|
220
|
+
return STATIC_REGEX_3$3.test(version.replace(V_PREFIX_RE, ""));
|
|
286
221
|
}
|
|
287
222
|
function compareSemver(a, b) {
|
|
288
223
|
if (a.major !== b.major) return a.major - b.major;
|
|
@@ -390,13 +325,13 @@ function generateReleaseIndex(releasesOrOpts, packageName) {
|
|
|
390
325
|
}
|
|
391
326
|
function isStubRelease(release) {
|
|
392
327
|
const body = (release.markdown || "").trim();
|
|
393
|
-
return body.length < 500 &&
|
|
328
|
+
return body.length < 500 && STATIC_REGEX_4$1.test(body);
|
|
394
329
|
}
|
|
395
330
|
async function fetchChangelog(owner, repo, ref, packageName) {
|
|
396
331
|
const paths = [];
|
|
397
332
|
if (packageName) {
|
|
398
|
-
const shortName = packageName.replace(
|
|
399
|
-
const scopeless = packageName.replace(
|
|
333
|
+
const shortName = packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "");
|
|
334
|
+
const scopeless = packageName.replace(NPM_SCOPE_PREFIX_RE, "").replace("/", "-");
|
|
400
335
|
const candidates = [...new Set([shortName, scopeless])];
|
|
401
336
|
for (const name of candidates) paths.push(`packages/${name}/CHANGELOG.md`);
|
|
402
337
|
}
|
|
@@ -430,6 +365,8 @@ async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageN
|
|
|
430
365
|
content: changelog
|
|
431
366
|
}];
|
|
432
367
|
}
|
|
368
|
+
const STATIC_REGEX_1$8 = /<h1[^>]*>([^<]+)<\/h1>/;
|
|
369
|
+
const STATIC_REGEX_2$2 = /<title>([^<]+)<\/title>/;
|
|
433
370
|
function formatBlogRelease(release) {
|
|
434
371
|
return `${[
|
|
435
372
|
"---",
|
|
@@ -449,10 +386,10 @@ async function fetchBlogPost(entry) {
|
|
|
449
386
|
}).catch(() => null);
|
|
450
387
|
if (!html) return null;
|
|
451
388
|
let title = "";
|
|
452
|
-
const titleMatch = html.match(
|
|
389
|
+
const titleMatch = html.match(STATIC_REGEX_1$8);
|
|
453
390
|
if (titleMatch) title = titleMatch[1].trim();
|
|
454
391
|
if (!title) {
|
|
455
|
-
const metaTitleMatch = html.match(
|
|
392
|
+
const metaTitleMatch = html.match(STATIC_REGEX_2$2);
|
|
456
393
|
if (metaTitleMatch) title = metaTitleMatch[1].trim();
|
|
457
394
|
}
|
|
458
395
|
const markdown = htmlToMarkdown(html);
|
|
@@ -499,342 +436,93 @@ async function fetchBlogReleases(packageName, installedVersion) {
|
|
|
499
436
|
content: formatBlogRelease(r)
|
|
500
437
|
}));
|
|
501
438
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
439
|
+
const STATIC_REGEX_1$7 = /github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[/#]|$)/;
|
|
440
|
+
const STATIC_REGEX_3$2 = /#.*$/;
|
|
441
|
+
const STATIC_REGEX_7 = /^git@github\.com:/;
|
|
442
|
+
const STATIC_REGEX_8 = /^(?:127\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/;
|
|
443
|
+
const STATIC_REGEX_9 = /^\[(?:f[cd]|fe[89ab]|::ffff:)/i;
|
|
444
|
+
function parseGitHubUrl(url) {
|
|
445
|
+
const match = url.match(STATIC_REGEX_1$7);
|
|
446
|
+
if (!match) return null;
|
|
447
|
+
return {
|
|
448
|
+
owner: match[1],
|
|
449
|
+
repo: match[2]
|
|
450
|
+
};
|
|
507
451
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
"
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
"proposal",
|
|
525
|
-
"rfc",
|
|
526
|
-
"idea",
|
|
527
|
-
"suggestion"
|
|
528
|
-
]);
|
|
529
|
-
const BUG_LABELS = new Set([
|
|
530
|
-
"bug",
|
|
531
|
-
"defect",
|
|
532
|
-
"regression",
|
|
533
|
-
"error",
|
|
534
|
-
"crash",
|
|
535
|
-
"fix",
|
|
536
|
-
"confirmed",
|
|
537
|
-
"verified"
|
|
538
|
-
]);
|
|
539
|
-
const QUESTION_LABELS = new Set([
|
|
540
|
-
"question",
|
|
541
|
-
"help wanted",
|
|
542
|
-
"support",
|
|
543
|
-
"usage",
|
|
544
|
-
"how-to",
|
|
545
|
-
"help",
|
|
546
|
-
"assistance"
|
|
547
|
-
]);
|
|
548
|
-
const DOCS_LABELS = new Set([
|
|
549
|
-
"documentation",
|
|
550
|
-
"docs",
|
|
551
|
-
"doc",
|
|
552
|
-
"typo"
|
|
553
|
-
]);
|
|
554
|
-
const labelRegexCache = /* @__PURE__ */ new WeakMap();
|
|
555
|
-
function getLabelRegex(keywords) {
|
|
556
|
-
let re = labelRegexCache.get(keywords);
|
|
557
|
-
if (!re) {
|
|
558
|
-
const escaped = Array.from(keywords, (k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
559
|
-
re = new RegExp(`\\b(?:${escaped.join("|")})\\b`);
|
|
560
|
-
labelRegexCache.set(keywords, re);
|
|
452
|
+
function parseGitHubRepoSlug(url) {
|
|
453
|
+
if (!url) return void 0;
|
|
454
|
+
const parsed = parseGitHubUrl(url);
|
|
455
|
+
return parsed ? `${parsed.owner}/${parsed.repo}` : void 0;
|
|
456
|
+
}
|
|
457
|
+
function parsePackageSpec(spec) {
|
|
458
|
+
if (spec.startsWith("@")) {
|
|
459
|
+
const slashIdx = spec.indexOf("/");
|
|
460
|
+
if (slashIdx !== -1) {
|
|
461
|
+
const atIdx = spec.indexOf("@", slashIdx + 1);
|
|
462
|
+
if (atIdx !== -1) return {
|
|
463
|
+
name: spec.slice(0, atIdx),
|
|
464
|
+
tag: spec.slice(atIdx + 1)
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return { name: spec };
|
|
561
468
|
}
|
|
562
|
-
|
|
469
|
+
const atIdx = spec.indexOf("@");
|
|
470
|
+
if (atIdx !== -1) return {
|
|
471
|
+
name: spec.slice(0, atIdx),
|
|
472
|
+
tag: spec.slice(atIdx + 1)
|
|
473
|
+
};
|
|
474
|
+
return { name: spec };
|
|
563
475
|
}
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
return getLabelRegex(keywords).test(label);
|
|
476
|
+
function normalizeRepoUrl(url) {
|
|
477
|
+
return url.replace(GIT_PLUS_PREFIX_RE, "").replace(STATIC_REGEX_3$2, "").replace(GIT_SUFFIX_RE, "").replace(GIT_PROTOCOL_PREFIX_RE, "https://").replace(GITHUB_SSH_URL_PREFIX_RE, "https://github.com").replace(STATIC_REGEX_7, "https://github.com/");
|
|
567
478
|
}
|
|
568
|
-
function
|
|
569
|
-
const
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
if (
|
|
573
|
-
|
|
574
|
-
return "other";
|
|
479
|
+
function extractBranchHint(url) {
|
|
480
|
+
const hash = url.indexOf("#");
|
|
481
|
+
if (hash === -1) return void 0;
|
|
482
|
+
const fragment = url.slice(hash + 1);
|
|
483
|
+
if (!fragment || fragment === "readme") return void 0;
|
|
484
|
+
return fragment;
|
|
575
485
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
486
|
+
function isGitHubRepoUrl(url) {
|
|
487
|
+
try {
|
|
488
|
+
const parsed = new URL(url);
|
|
489
|
+
return parsed.hostname === "github.com" || parsed.hostname === "www.github.com";
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
580
493
|
}
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
494
|
+
function isLikelyCodeHostUrl(url) {
|
|
495
|
+
if (!url) return false;
|
|
496
|
+
try {
|
|
497
|
+
const parsed = new URL(url);
|
|
498
|
+
return [
|
|
499
|
+
"github.com",
|
|
500
|
+
"www.github.com",
|
|
501
|
+
"gitlab.com",
|
|
502
|
+
"www.gitlab.com"
|
|
503
|
+
].includes(parsed.hostname);
|
|
504
|
+
} catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
586
507
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const group = byType.get(type) || [];
|
|
606
|
-
const take = Math.min(quota, group.length, remaining);
|
|
607
|
-
for (let i = 0; i < take; i++) {
|
|
608
|
-
selected.push(group[i]);
|
|
609
|
-
used.add(group[i].number);
|
|
610
|
-
remaining--;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (remaining > 0) {
|
|
614
|
-
const unused = issues.filter((i) => !used.has(i.number) && i.type !== "feature").sort((a, b) => b.score - a.score);
|
|
615
|
-
for (const issue of unused) {
|
|
616
|
-
if (remaining <= 0) break;
|
|
617
|
-
selected.push(issue);
|
|
618
|
-
remaining--;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return selected.sort((a, b) => b.score - a.score);
|
|
622
|
-
}
|
|
623
|
-
function bodyLimit(reactions) {
|
|
624
|
-
if (reactions >= 10) return 2e3;
|
|
625
|
-
if (reactions >= 5) return 1500;
|
|
626
|
-
return 800;
|
|
627
|
-
}
|
|
628
|
-
function fetchIssuesByState(owner, repo, state, count, releasedAt, fromDate) {
|
|
629
|
-
const fetchCount = Math.min(count * 3, 100);
|
|
630
|
-
let datePart = "";
|
|
631
|
-
if (fromDate) datePart = state === "closed" ? `+closed:>=${fromDate}` : `+created:>=${fromDate}`;
|
|
632
|
-
else if (state === "closed") if (releasedAt) {
|
|
633
|
-
datePart = `+closed:>=${isoDate(releasedAt)}`;
|
|
634
|
-
const cap = new Date(releasedAt);
|
|
635
|
-
cap.setMonth(cap.getMonth() + 6);
|
|
636
|
-
if (cap < /* @__PURE__ */ new Date()) datePart += `+closed:<=${isoDate(cap.toISOString())}`;
|
|
637
|
-
} else datePart = `+closed:>${oneYearAgo()}`;
|
|
638
|
-
else if (releasedAt) {
|
|
639
|
-
const date = new Date(releasedAt);
|
|
640
|
-
date.setMonth(date.getMonth() + 6);
|
|
641
|
-
datePart = `+created:<=${isoDate(date.toISOString())}`;
|
|
642
|
-
}
|
|
643
|
-
const { stdout: result } = spawnSync("gh", [
|
|
644
|
-
"api",
|
|
645
|
-
`search/issues?q=${`repo:${owner}/${repo}+is:issue+is:${state}${datePart}`}&sort=reactions&order=desc&per_page=${fetchCount}`,
|
|
646
|
-
"-q",
|
|
647
|
-
".items[] | {number, title, state, labels: [.labels[]?.name], body, createdAt: .created_at, url: .html_url, reactions: .reactions[\"+1\"], comments: .comments, user: .user.login, userType: .user.type, authorAssociation: .author_association}"
|
|
648
|
-
], {
|
|
649
|
-
encoding: "utf-8",
|
|
650
|
-
maxBuffer: 10 * 1024 * 1024
|
|
651
|
-
});
|
|
652
|
-
if (!result) return [];
|
|
653
|
-
return result.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((issue) => !BOT_USERS.has(issue.user) && issue.userType !== "Bot").filter((issue) => !isNoiseIssue(issue)).filter((issue) => !isNonTechnical(issue)).map(({ user: _, userType: __, authorAssociation, ...issue }) => {
|
|
654
|
-
const isMaintainer = [
|
|
655
|
-
"OWNER",
|
|
656
|
-
"MEMBER",
|
|
657
|
-
"COLLABORATOR"
|
|
658
|
-
].includes(authorAssociation);
|
|
659
|
-
const isRoadmap = /\broadmap\b/i.test(issue.title) || issue.labels.some((l) => /roadmap/i.test(l));
|
|
660
|
-
return {
|
|
661
|
-
...issue,
|
|
662
|
-
type: classifyIssue(issue.labels),
|
|
663
|
-
topComments: [],
|
|
664
|
-
score: freshnessScore(issue.reactions, issue.createdAt) * (isMaintainer && isRoadmap ? 5 : 1)
|
|
665
|
-
};
|
|
666
|
-
}).sort((a, b) => b.score - a.score).slice(0, count);
|
|
667
|
-
}
|
|
668
|
-
function oneYearAgo() {
|
|
669
|
-
const d = /* @__PURE__ */ new Date();
|
|
670
|
-
d.setFullYear(d.getFullYear() - 1);
|
|
671
|
-
return isoDate(d.toISOString());
|
|
672
|
-
}
|
|
673
|
-
function enrichWithComments(owner, repo, issues, topN = 15) {
|
|
674
|
-
const worth = issues.filter((i) => i.comments > 0 && (i.type === "bug" || i.type === "question" || i.reactions >= 3)).sort((a, b) => b.score - a.score).slice(0, topN);
|
|
675
|
-
if (worth.length === 0) return;
|
|
676
|
-
const query = `query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { ${worth.map((issue, i) => `i${i}: issue(number: ${issue.number}) { comments(first: 10) { nodes { body author { login } authorAssociation reactions { totalCount } } } }`).join(" ")} } }`;
|
|
677
|
-
try {
|
|
678
|
-
const { stdout: result } = spawnSync("gh", [
|
|
679
|
-
"api",
|
|
680
|
-
"graphql",
|
|
681
|
-
"-f",
|
|
682
|
-
`query=${query}`,
|
|
683
|
-
"-f",
|
|
684
|
-
`owner=${owner}`,
|
|
685
|
-
"-f",
|
|
686
|
-
`repo=${repo}`
|
|
687
|
-
], {
|
|
688
|
-
encoding: "utf-8",
|
|
689
|
-
maxBuffer: 10 * 1024 * 1024
|
|
690
|
-
});
|
|
691
|
-
if (!result) return;
|
|
692
|
-
const repo_ = JSON.parse(result)?.data?.repository;
|
|
693
|
-
if (!repo_) return;
|
|
694
|
-
for (let i = 0; i < worth.length; i++) {
|
|
695
|
-
const nodes = repo_[`i${i}`]?.comments?.nodes;
|
|
696
|
-
if (!Array.isArray(nodes)) continue;
|
|
697
|
-
const issue = worth[i];
|
|
698
|
-
const comments = nodes.filter((c) => c.author && !BOT_USERS.has(c.author.login)).filter((c) => !COMMENT_NOISE_RE.test((c.body || "").trim())).map((c) => {
|
|
699
|
-
const isMaintainer = [
|
|
700
|
-
"OWNER",
|
|
701
|
-
"MEMBER",
|
|
702
|
-
"COLLABORATOR"
|
|
703
|
-
].includes(c.authorAssociation);
|
|
704
|
-
const body = c.body || "";
|
|
705
|
-
const reactions = c.reactions?.totalCount || 0;
|
|
706
|
-
const _score = (isMaintainer ? 3 : 1) * (hasCodeBlock(body) ? 2 : 1) * (1 + reactions);
|
|
707
|
-
return {
|
|
708
|
-
body,
|
|
709
|
-
author: c.author.login,
|
|
710
|
-
reactions,
|
|
711
|
-
isMaintainer,
|
|
712
|
-
_score
|
|
713
|
-
};
|
|
714
|
-
}).sort((a, b) => b._score - a._score);
|
|
715
|
-
issue.topComments = comments.slice(0, 3).map(({ _score: _, ...c }) => c);
|
|
716
|
-
if (issue.state === "closed") issue.resolvedIn = detectResolvedVersion(comments);
|
|
717
|
-
}
|
|
718
|
-
} catch {}
|
|
719
|
-
}
|
|
720
|
-
function detectResolvedVersion(comments) {
|
|
721
|
-
const maintainerComments = comments.filter((c) => c.isMaintainer);
|
|
722
|
-
for (const c of maintainerComments.reverse()) {
|
|
723
|
-
const match = c.body.match(/(?:fixed|landed|released|available|shipped|resolved|included)\s+in\s+v?(\d+\.\d+(?:\.\d+)?)/i);
|
|
724
|
-
if (match) return match[1];
|
|
725
|
-
if (c.body.length < 100) {
|
|
726
|
-
const vMatch = c.body.match(/\bv?(\d+\.\d+\.\d+)\b/);
|
|
727
|
-
if (vMatch) return vMatch[1];
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
async function fetchGitHubIssues(owner, repo, limit = 30, releasedAt, fromDate) {
|
|
732
|
-
if (!isGhAvailable()) return [];
|
|
733
|
-
const openCount = Math.ceil(limit * .75);
|
|
734
|
-
const closedCount = limit - openCount;
|
|
735
|
-
try {
|
|
736
|
-
const open = fetchIssuesByState(owner, repo, "open", Math.min(openCount * 2, 100), releasedAt, fromDate);
|
|
737
|
-
const closed = fetchIssuesByState(owner, repo, "closed", Math.min(closedCount * 2, 50), releasedAt, fromDate);
|
|
738
|
-
const selected = applyTypeQuotas([...open, ...closed], limit);
|
|
739
|
-
enrichWithComments(owner, repo, selected);
|
|
740
|
-
return selected;
|
|
741
|
-
} catch {
|
|
742
|
-
return [];
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
function formatIssueAsMarkdown(issue) {
|
|
746
|
-
const limit = bodyLimit(issue.reactions);
|
|
747
|
-
const fmFields = {
|
|
748
|
-
number: issue.number,
|
|
749
|
-
title: issue.title,
|
|
750
|
-
type: issue.type,
|
|
751
|
-
state: issue.state,
|
|
752
|
-
created: isoDate(issue.createdAt),
|
|
753
|
-
url: issue.url,
|
|
754
|
-
reactions: issue.reactions,
|
|
755
|
-
comments: issue.comments
|
|
756
|
-
};
|
|
757
|
-
if (issue.resolvedIn) fmFields.resolvedIn = issue.resolvedIn;
|
|
758
|
-
if (issue.labels.length > 0) fmFields.labels = `[${issue.labels.join(", ")}]`;
|
|
759
|
-
const lines = [
|
|
760
|
-
buildFrontmatter(fmFields),
|
|
761
|
-
"",
|
|
762
|
-
`# ${issue.title}`
|
|
763
|
-
];
|
|
764
|
-
if (issue.body) {
|
|
765
|
-
const body = truncateBody(issue.body, limit);
|
|
766
|
-
lines.push("", body);
|
|
767
|
-
}
|
|
768
|
-
if (issue.topComments.length > 0) {
|
|
769
|
-
lines.push("", "---", "", "## Top Comments");
|
|
770
|
-
for (const c of issue.topComments) {
|
|
771
|
-
const reactions = c.reactions > 0 ? ` (+${c.reactions})` : "";
|
|
772
|
-
const maintainer = c.isMaintainer ? " [maintainer]" : "";
|
|
773
|
-
const commentBody = truncateBody(c.body, 600);
|
|
774
|
-
lines.push("", `**@${c.author}**${maintainer}${reactions}:`, "", commentBody);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
return lines.join("\n");
|
|
778
|
-
}
|
|
779
|
-
function generateIssueIndex(issues) {
|
|
780
|
-
const byType = /* @__PURE__ */ new Map();
|
|
781
|
-
for (const issue of issues) mapInsert(byType, issue.type, () => []).push(issue);
|
|
782
|
-
const typeLabels = {
|
|
783
|
-
bug: "Bugs & Regressions",
|
|
784
|
-
question: "Questions & Usage Help",
|
|
785
|
-
docs: "Documentation",
|
|
786
|
-
feature: "Feature Requests",
|
|
787
|
-
other: "Other"
|
|
788
|
-
};
|
|
789
|
-
const typeOrder = [
|
|
790
|
-
"bug",
|
|
791
|
-
"question",
|
|
792
|
-
"docs",
|
|
793
|
-
"other",
|
|
794
|
-
"feature"
|
|
795
|
-
];
|
|
796
|
-
const sections = [
|
|
797
|
-
[
|
|
798
|
-
"---",
|
|
799
|
-
`total: ${issues.length}`,
|
|
800
|
-
`open: ${issues.filter((i) => i.state === "open").length}`,
|
|
801
|
-
`closed: ${issues.filter((i) => i.state !== "open").length}`,
|
|
802
|
-
"---"
|
|
803
|
-
].join("\n"),
|
|
804
|
-
"",
|
|
805
|
-
"# Issues Index",
|
|
806
|
-
""
|
|
807
|
-
];
|
|
808
|
-
for (const type of typeOrder) {
|
|
809
|
-
const group = byType.get(type);
|
|
810
|
-
if (!group?.length) continue;
|
|
811
|
-
sections.push(`## ${typeLabels[type]} (${group.length})`, "");
|
|
812
|
-
for (const issue of group) {
|
|
813
|
-
const reactions = issue.reactions > 0 ? ` (+${issue.reactions})` : "";
|
|
814
|
-
const state = issue.state === "open" ? "" : " [closed]";
|
|
815
|
-
const resolved = issue.resolvedIn ? ` [fixed in ${issue.resolvedIn}]` : "";
|
|
816
|
-
const date = isoDate(issue.createdAt);
|
|
817
|
-
sections.push(`- [#${issue.number}](./issue-${issue.number}.md): ${issue.title}${reactions}${state}${resolved} (${date})`);
|
|
818
|
-
}
|
|
819
|
-
sections.push("");
|
|
820
|
-
}
|
|
821
|
-
return sections.join("\n");
|
|
822
|
-
}
|
|
823
|
-
async function fetchLlmsUrl(docsUrl) {
|
|
824
|
-
const llmsUrl = `${new URL(docsUrl).origin}/llms.txt`;
|
|
825
|
-
if (await verifyUrl(llmsUrl)) return llmsUrl;
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
async function fetchLlmsTxt(url) {
|
|
829
|
-
const content = await fetchText(url);
|
|
830
|
-
if (!content || content.length < 50) return null;
|
|
831
|
-
return {
|
|
832
|
-
raw: content,
|
|
833
|
-
links: parseMarkdownLinks(content)
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
function parseMarkdownLinks(content) {
|
|
837
|
-
return extractLinks(content).filter((l) => l.url.endsWith(".md"));
|
|
508
|
+
const USELESS_HOSTS = new Set([
|
|
509
|
+
"twitter.com",
|
|
510
|
+
"x.com",
|
|
511
|
+
"facebook.com",
|
|
512
|
+
"linkedin.com",
|
|
513
|
+
"youtube.com",
|
|
514
|
+
"instagram.com",
|
|
515
|
+
"npmjs.com",
|
|
516
|
+
"www.npmjs.com",
|
|
517
|
+
"yarnpkg.com"
|
|
518
|
+
]);
|
|
519
|
+
function isUselessDocsUrl(url) {
|
|
520
|
+
try {
|
|
521
|
+
const { hostname } = new URL(url);
|
|
522
|
+
return USELESS_HOSTS.has(hostname);
|
|
523
|
+
} catch {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
838
526
|
}
|
|
839
527
|
function isSafeUrl(url) {
|
|
840
528
|
try {
|
|
@@ -842,66 +530,21 @@ function isSafeUrl(url) {
|
|
|
842
530
|
if (parsed.protocol !== "https:") return false;
|
|
843
531
|
const host = parsed.hostname;
|
|
844
532
|
if (host === "localhost" || host === "0.0.0.0" || host === "[::1]") return false;
|
|
845
|
-
if (
|
|
846
|
-
if (
|
|
533
|
+
if (STATIC_REGEX_8.test(host)) return false;
|
|
534
|
+
if (STATIC_REGEX_9.test(host)) return false;
|
|
847
535
|
return true;
|
|
848
536
|
} catch {
|
|
849
537
|
return false;
|
|
850
538
|
}
|
|
851
539
|
}
|
|
852
|
-
async function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (content && content.length > 100) return {
|
|
861
|
-
url: link.url.startsWith("http") ? new URL(link.url).pathname : link.url,
|
|
862
|
-
title: link.title,
|
|
863
|
-
content
|
|
864
|
-
};
|
|
865
|
-
return null;
|
|
866
|
-
})))).filter((d) => d !== null);
|
|
867
|
-
}
|
|
868
|
-
function normalizeLlmsLinks(content, baseUrl) {
|
|
869
|
-
let normalized = content;
|
|
870
|
-
if (baseUrl) {
|
|
871
|
-
const escaped = baseUrl.replace(/\/$/, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
872
|
-
normalized = normalized.replace(new RegExp(`\\]\\(${escaped}(/[^)]+\\.md)\\)`, "g"), "](./docs$1)");
|
|
873
|
-
}
|
|
874
|
-
normalized = normalized.replace(/\]\(\/([^)]+\.md)\)/g, "](./docs/$1)");
|
|
875
|
-
return normalized;
|
|
876
|
-
}
|
|
877
|
-
function extractSections(content, patterns) {
|
|
878
|
-
const sections = [];
|
|
879
|
-
const parts = content.split(/\n---\n/);
|
|
880
|
-
for (const part of parts) {
|
|
881
|
-
const urlMatch = part.match(/^url: *(\S.*)$/m);
|
|
882
|
-
if (!urlMatch) continue;
|
|
883
|
-
const url = urlMatch[1];
|
|
884
|
-
if (patterns.some((p) => url.includes(p))) {
|
|
885
|
-
const contentStart = part.indexOf("\n", part.indexOf("url:"));
|
|
886
|
-
if (contentStart > -1) sections.push(part.slice(contentStart + 1));
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
if (sections.length === 0) return null;
|
|
890
|
-
return sections.join("\n\n---\n\n");
|
|
891
|
-
}
|
|
892
|
-
const MIN_GIT_DOCS = 5;
|
|
893
|
-
const isShallowGitDocs = (n) => n > 0 && n < 5;
|
|
894
|
-
async function listFilesAtRef(owner, repo, ref) {
|
|
895
|
-
if (!isKnownPrivateRepo(owner, repo)) {
|
|
896
|
-
const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/files/${ref}`).catch(() => null);
|
|
897
|
-
if (data?.files?.length) return data.files.map((f) => f.path);
|
|
898
|
-
}
|
|
899
|
-
const tree = await ghApi(`repos/${owner}/${repo}/git/trees/${ref}?recursive=1`);
|
|
900
|
-
if (tree?.tree?.length) {
|
|
901
|
-
markRepoPrivate(owner, repo);
|
|
902
|
-
return tree.tree.map((f) => f.path);
|
|
903
|
-
}
|
|
904
|
-
return [];
|
|
540
|
+
async function listFilesAtRef(owner, repo, ref) {
|
|
541
|
+
return await fetchUnghOrApi(owner, repo, async () => {
|
|
542
|
+
const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/files/${ref}`);
|
|
543
|
+
return data.files?.length ? data.files.map((f) => f.path) : null;
|
|
544
|
+
}, async () => {
|
|
545
|
+
const tree = await ghApi(`repos/${owner}/${repo}/git/trees/${ref}?recursive=1`);
|
|
546
|
+
return tree?.tree?.length ? tree.tree.map((f) => f.path) : null;
|
|
547
|
+
}) ?? [];
|
|
905
548
|
}
|
|
906
549
|
async function findGitTag(owner, repo, version, packageName, branchHint) {
|
|
907
550
|
const candidates = [`v${version}`, version];
|
|
@@ -935,26 +578,25 @@ async function findGitTag(owner, repo, version, packageName, branchHint) {
|
|
|
935
578
|
return null;
|
|
936
579
|
}
|
|
937
580
|
async function fetchUnghReleases(owner, repo) {
|
|
938
|
-
|
|
939
|
-
const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`)
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
markRepoPrivate(owner, repo);
|
|
945
|
-
return raw.map((r) => ({
|
|
581
|
+
return await fetchUnghOrApi(owner, repo, async () => {
|
|
582
|
+
const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`);
|
|
583
|
+
return data.releases?.length ? data.releases : null;
|
|
584
|
+
}, async () => {
|
|
585
|
+
const raw = await ghApiPaginated(`repos/${owner}/${repo}/releases`);
|
|
586
|
+
return raw.length > 0 ? raw.map((r) => ({
|
|
946
587
|
tag: r.tag_name,
|
|
947
588
|
publishedAt: r.published_at
|
|
948
|
-
}));
|
|
949
|
-
}
|
|
950
|
-
return [];
|
|
589
|
+
})) : null;
|
|
590
|
+
}) ?? [];
|
|
951
591
|
}
|
|
952
592
|
async function findLatestReleaseTag(owner, repo, packageName) {
|
|
953
593
|
const prefix = `${packageName}@`;
|
|
954
594
|
return (await fetchUnghReleases(owner, repo)).find((r) => r.tag.startsWith(prefix))?.tag ?? null;
|
|
955
595
|
}
|
|
596
|
+
const STATIC_REGEX_1$6 = /\.(?:md|mdx)$/;
|
|
597
|
+
const isShallowGitDocs = (n) => n > 0 && n < 5;
|
|
956
598
|
function filterDocFiles(files, pathPrefix) {
|
|
957
|
-
return files.filter((f) => f.startsWith(pathPrefix) &&
|
|
599
|
+
return files.filter((f) => f.startsWith(pathPrefix) && STATIC_REGEX_1$6.test(f));
|
|
958
600
|
}
|
|
959
601
|
const FRAMEWORK_NAMES = new Set([
|
|
960
602
|
"vue",
|
|
@@ -968,7 +610,7 @@ const FRAMEWORK_NAMES = new Set([
|
|
|
968
610
|
]);
|
|
969
611
|
function filterFrameworkDocs(files, packageName) {
|
|
970
612
|
if (!packageName) return files;
|
|
971
|
-
const shortName = packageName.replace(
|
|
613
|
+
const shortName = packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "");
|
|
972
614
|
const targetFramework = [...FRAMEWORK_NAMES].find((fw) => shortName.includes(fw));
|
|
973
615
|
if (!targetFramework) return files;
|
|
974
616
|
const otherFrameworks = [...FRAMEWORK_NAMES].filter((fw) => fw !== targetFramework);
|
|
@@ -1025,7 +667,7 @@ function scoreDocDir(dir, fileCount) {
|
|
|
1025
667
|
return fileCount * (hasDocDirBonus(dir) ? 1.5 : 1) / depth;
|
|
1026
668
|
}
|
|
1027
669
|
function discoverDocFiles(allFiles, packageName) {
|
|
1028
|
-
const mdFiles = allFiles.filter((f) =>
|
|
670
|
+
const mdFiles = allFiles.filter((f) => STATIC_REGEX_1$6.test(f)).filter((f) => !NOISE_PATTERNS.some((p) => p.test(f))).filter((f) => f.includes("/"));
|
|
1029
671
|
if (packageName?.includes("/")) {
|
|
1030
672
|
const subPkgPrefix = `packages/${packageName.split("/").pop().toLowerCase()}/`;
|
|
1031
673
|
const subPkgFiles = mdFiles.filter((f) => f.startsWith(subPkgPrefix));
|
|
@@ -1103,51 +745,418 @@ async function fetchGitDocs(owner, repo, version, packageName, repoUrl) {
|
|
|
1103
745
|
allFiles = tag.files;
|
|
1104
746
|
}
|
|
1105
747
|
}
|
|
1106
|
-
docs = filterFrameworkDocs(docs, packageName);
|
|
1107
|
-
if (docs.length === 0) return null;
|
|
748
|
+
docs = filterFrameworkDocs(docs, packageName);
|
|
749
|
+
if (docs.length === 0) return null;
|
|
750
|
+
return {
|
|
751
|
+
baseUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${tag.ref}`,
|
|
752
|
+
ref: tag.ref,
|
|
753
|
+
files: docs,
|
|
754
|
+
docsPrefix,
|
|
755
|
+
allFiles,
|
|
756
|
+
fallback: tag.fallback
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function normalizePath(p) {
|
|
760
|
+
return p.replace(LEADING_SLASH_RE, "").replace(STATIC_REGEX_1$6, "");
|
|
761
|
+
}
|
|
762
|
+
function validateGitDocsWithLlms(llmsLinks, repoFiles) {
|
|
763
|
+
if (llmsLinks.length === 0) return {
|
|
764
|
+
isValid: true,
|
|
765
|
+
matchRatio: 1
|
|
766
|
+
};
|
|
767
|
+
const sample = llmsLinks.slice(0, 10);
|
|
768
|
+
const normalizedLinks = sample.map((link) => {
|
|
769
|
+
let path = link.url;
|
|
770
|
+
if (path.startsWith("http")) try {
|
|
771
|
+
path = new URL(path).pathname;
|
|
772
|
+
} catch {}
|
|
773
|
+
return normalizePath(path);
|
|
774
|
+
});
|
|
775
|
+
const repoNormalized = new Set(repoFiles.map(normalizePath));
|
|
776
|
+
let matches = 0;
|
|
777
|
+
for (const linkPath of normalizedLinks) for (const repoPath of repoNormalized) if (repoPath === linkPath || repoPath.endsWith(`/${linkPath}`)) {
|
|
778
|
+
matches++;
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
const matchRatio = matches / sample.length;
|
|
782
|
+
return {
|
|
783
|
+
isValid: matchRatio >= .3,
|
|
784
|
+
matchRatio
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const STATIC_REGEX_1$5 = /\b(?:love|thank|awesome|great work)\b/i;
|
|
788
|
+
const STATIC_REGEX_2$1 = /\broadmap\b/i;
|
|
789
|
+
const STATIC_REGEX_3$1 = /roadmap/i;
|
|
790
|
+
const STATIC_REGEX_4 = /(?:fixed|landed|released|available|shipped|resolved|included)\s+in\s+v?(\d+\.\d+(?:\.\d+)?)/i;
|
|
791
|
+
const STATIC_REGEX_5 = /\bv?(\d+\.\d+\.\d+)\b/;
|
|
792
|
+
let _ghAvailable;
|
|
793
|
+
function isGhAvailable() {
|
|
794
|
+
if (_ghAvailable !== void 0) return _ghAvailable;
|
|
795
|
+
const { status } = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
|
|
796
|
+
return _ghAvailable = status === 0;
|
|
797
|
+
}
|
|
798
|
+
const NOISE_LABELS = new Set([
|
|
799
|
+
"duplicate",
|
|
800
|
+
"stale",
|
|
801
|
+
"invalid",
|
|
802
|
+
"wontfix",
|
|
803
|
+
"won't fix",
|
|
804
|
+
"spam",
|
|
805
|
+
"off-topic",
|
|
806
|
+
"needs triage",
|
|
807
|
+
"triage"
|
|
808
|
+
]);
|
|
809
|
+
const FEATURE_LABELS = new Set([
|
|
810
|
+
"enhancement",
|
|
811
|
+
"feature",
|
|
812
|
+
"feature request",
|
|
813
|
+
"feature-request",
|
|
814
|
+
"proposal",
|
|
815
|
+
"rfc",
|
|
816
|
+
"idea",
|
|
817
|
+
"suggestion"
|
|
818
|
+
]);
|
|
819
|
+
const BUG_LABELS = new Set([
|
|
820
|
+
"bug",
|
|
821
|
+
"defect",
|
|
822
|
+
"regression",
|
|
823
|
+
"error",
|
|
824
|
+
"crash",
|
|
825
|
+
"fix",
|
|
826
|
+
"confirmed",
|
|
827
|
+
"verified"
|
|
828
|
+
]);
|
|
829
|
+
const QUESTION_LABELS = new Set([
|
|
830
|
+
"question",
|
|
831
|
+
"help wanted",
|
|
832
|
+
"support",
|
|
833
|
+
"usage",
|
|
834
|
+
"how-to",
|
|
835
|
+
"help",
|
|
836
|
+
"assistance"
|
|
837
|
+
]);
|
|
838
|
+
const DOCS_LABELS = new Set([
|
|
839
|
+
"documentation",
|
|
840
|
+
"docs",
|
|
841
|
+
"doc",
|
|
842
|
+
"typo"
|
|
843
|
+
]);
|
|
844
|
+
const labelRegexCache = /* @__PURE__ */ new WeakMap();
|
|
845
|
+
function getLabelRegex(keywords) {
|
|
846
|
+
let re = labelRegexCache.get(keywords);
|
|
847
|
+
if (!re) {
|
|
848
|
+
const escaped = Array.from(keywords, (k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
849
|
+
re = new RegExp(`\\b(?:${escaped.join("|")})\\b`);
|
|
850
|
+
labelRegexCache.set(keywords, re);
|
|
851
|
+
}
|
|
852
|
+
return re;
|
|
853
|
+
}
|
|
854
|
+
function labelMatchesAny(label, keywords) {
|
|
855
|
+
if (keywords.has(label)) return true;
|
|
856
|
+
return getLabelRegex(keywords).test(label);
|
|
857
|
+
}
|
|
858
|
+
function classifyIssue(labels) {
|
|
859
|
+
const lower = labels.map((l) => l.toLowerCase());
|
|
860
|
+
if (lower.some((l) => labelMatchesAny(l, BUG_LABELS))) return "bug";
|
|
861
|
+
if (lower.some((l) => labelMatchesAny(l, QUESTION_LABELS))) return "question";
|
|
862
|
+
if (lower.some((l) => labelMatchesAny(l, DOCS_LABELS))) return "docs";
|
|
863
|
+
if (lower.some((l) => labelMatchesAny(l, FEATURE_LABELS))) return "feature";
|
|
864
|
+
return "other";
|
|
865
|
+
}
|
|
866
|
+
function isNoiseIssue(issue) {
|
|
867
|
+
if (issue.labels.map((l) => l.toLowerCase()).some((l) => labelMatchesAny(l, NOISE_LABELS))) return true;
|
|
868
|
+
if (issue.title.startsWith("☂️") || issue.title.startsWith("[META]") || issue.title.startsWith("[Tracking]")) return true;
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
function isNonTechnical(issue) {
|
|
872
|
+
const body = (issue.body || "").trim();
|
|
873
|
+
if (body.length < 200 && !hasCodeBlock(body) && issue.reactions > 50) return true;
|
|
874
|
+
if (STATIC_REGEX_1$5.test(issue.title) && !hasCodeBlock(body)) return true;
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
function freshnessScore(reactions, createdAt) {
|
|
878
|
+
return reactions * (1 / (1 + (Date.now() - new Date(createdAt).getTime()) / (365.25 * 24 * 60 * 60 * 1e3) * .6));
|
|
879
|
+
}
|
|
880
|
+
function applyTypeQuotas(issues, limit) {
|
|
881
|
+
const byType = /* @__PURE__ */ new Map();
|
|
882
|
+
for (const issue of issues) mapInsert(byType, issue.type, () => []).push(issue);
|
|
883
|
+
for (const group of byType.values()) group.sort((a, b) => b.score - a.score);
|
|
884
|
+
const quotas = [
|
|
885
|
+
["bug", Math.ceil(limit * .4)],
|
|
886
|
+
["question", Math.ceil(limit * .3)],
|
|
887
|
+
["docs", Math.ceil(limit * .15)],
|
|
888
|
+
["feature", Math.ceil(limit * .1)],
|
|
889
|
+
["other", Math.ceil(limit * .05)]
|
|
890
|
+
];
|
|
891
|
+
const selected = [];
|
|
892
|
+
const used = /* @__PURE__ */ new Set();
|
|
893
|
+
let remaining = limit;
|
|
894
|
+
for (const [type, quota] of quotas) {
|
|
895
|
+
const group = byType.get(type) || [];
|
|
896
|
+
const take = Math.min(quota, group.length, remaining);
|
|
897
|
+
for (let i = 0; i < take; i++) {
|
|
898
|
+
selected.push(group[i]);
|
|
899
|
+
used.add(group[i].number);
|
|
900
|
+
remaining--;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (remaining > 0) {
|
|
904
|
+
const unused = issues.filter((i) => !used.has(i.number) && i.type !== "feature").sort((a, b) => b.score - a.score);
|
|
905
|
+
for (const issue of unused) {
|
|
906
|
+
if (remaining <= 0) break;
|
|
907
|
+
selected.push(issue);
|
|
908
|
+
remaining--;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return selected.sort((a, b) => b.score - a.score);
|
|
912
|
+
}
|
|
913
|
+
function bodyLimit(reactions) {
|
|
914
|
+
if (reactions >= 10) return 2e3;
|
|
915
|
+
if (reactions >= 5) return 1500;
|
|
916
|
+
return 800;
|
|
917
|
+
}
|
|
918
|
+
function fetchIssuesByState(owner, repo, state, count, releasedAt, fromDate) {
|
|
919
|
+
const fetchCount = Math.min(count * 3, 100);
|
|
920
|
+
let datePart = "";
|
|
921
|
+
if (fromDate) datePart = state === "closed" ? `+closed:>=${fromDate}` : `+created:>=${fromDate}`;
|
|
922
|
+
else if (state === "closed") if (releasedAt) {
|
|
923
|
+
datePart = `+closed:>=${isoDate(releasedAt)}`;
|
|
924
|
+
const cap = new Date(releasedAt);
|
|
925
|
+
cap.setMonth(cap.getMonth() + 6);
|
|
926
|
+
if (cap < /* @__PURE__ */ new Date()) datePart += `+closed:<=${isoDate(cap.toISOString())}`;
|
|
927
|
+
} else datePart = `+closed:>${oneYearAgo()}`;
|
|
928
|
+
else if (releasedAt) {
|
|
929
|
+
const date = new Date(releasedAt);
|
|
930
|
+
date.setMonth(date.getMonth() + 6);
|
|
931
|
+
datePart = `+created:<=${isoDate(date.toISOString())}`;
|
|
932
|
+
}
|
|
933
|
+
const { stdout: result } = spawnSync("gh", [
|
|
934
|
+
"api",
|
|
935
|
+
`search/issues?q=${`repo:${owner}/${repo}+is:issue+is:${state}${datePart}`}&sort=reactions&order=desc&per_page=${fetchCount}`,
|
|
936
|
+
"-q",
|
|
937
|
+
".items[] | {number, title, state, labels: [.labels[]?.name], body, createdAt: .created_at, url: .html_url, reactions: .reactions[\"+1\"], comments: .comments, user: .user.login, userType: .user.type, authorAssociation: .author_association}"
|
|
938
|
+
], {
|
|
939
|
+
encoding: "utf-8",
|
|
940
|
+
maxBuffer: 10 * 1024 * 1024
|
|
941
|
+
});
|
|
942
|
+
if (!result) return [];
|
|
943
|
+
return result.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((issue) => !BOT_USERS.has(issue.user) && issue.userType !== "Bot").filter((issue) => !isNoiseIssue(issue)).filter((issue) => !isNonTechnical(issue)).map(({ user: _, userType: __, authorAssociation, ...issue }) => {
|
|
944
|
+
const isMaintainer = [
|
|
945
|
+
"OWNER",
|
|
946
|
+
"MEMBER",
|
|
947
|
+
"COLLABORATOR"
|
|
948
|
+
].includes(authorAssociation);
|
|
949
|
+
const isRoadmap = STATIC_REGEX_2$1.test(issue.title) || issue.labels.some((l) => STATIC_REGEX_3$1.test(l));
|
|
950
|
+
return {
|
|
951
|
+
...issue,
|
|
952
|
+
type: classifyIssue(issue.labels),
|
|
953
|
+
topComments: [],
|
|
954
|
+
score: freshnessScore(issue.reactions, issue.createdAt) * (isMaintainer && isRoadmap ? 5 : 1)
|
|
955
|
+
};
|
|
956
|
+
}).sort((a, b) => b.score - a.score).slice(0, count);
|
|
957
|
+
}
|
|
958
|
+
function oneYearAgo() {
|
|
959
|
+
const d = /* @__PURE__ */ new Date();
|
|
960
|
+
d.setFullYear(d.getFullYear() - 1);
|
|
961
|
+
return isoDate(d.toISOString());
|
|
962
|
+
}
|
|
963
|
+
function enrichWithComments(owner, repo, issues, topN = 15) {
|
|
964
|
+
const worth = issues.filter((i) => i.comments > 0 && (i.type === "bug" || i.type === "question" || i.reactions >= 3)).sort((a, b) => b.score - a.score).slice(0, topN);
|
|
965
|
+
if (worth.length === 0) return;
|
|
966
|
+
const query = `query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { ${worth.map((issue, i) => `i${i}: issue(number: ${issue.number}) { comments(first: 10) { nodes { body author { login } authorAssociation reactions { totalCount } } } }`).join(" ")} } }`;
|
|
967
|
+
try {
|
|
968
|
+
const { stdout: result } = spawnSync("gh", [
|
|
969
|
+
"api",
|
|
970
|
+
"graphql",
|
|
971
|
+
"-f",
|
|
972
|
+
`query=${query}`,
|
|
973
|
+
"-f",
|
|
974
|
+
`owner=${owner}`,
|
|
975
|
+
"-f",
|
|
976
|
+
`repo=${repo}`
|
|
977
|
+
], {
|
|
978
|
+
encoding: "utf-8",
|
|
979
|
+
maxBuffer: 10 * 1024 * 1024
|
|
980
|
+
});
|
|
981
|
+
if (!result) return;
|
|
982
|
+
const repo_ = JSON.parse(result)?.data?.repository;
|
|
983
|
+
if (!repo_) return;
|
|
984
|
+
for (let i = 0; i < worth.length; i++) {
|
|
985
|
+
const nodes = repo_[`i${i}`]?.comments?.nodes;
|
|
986
|
+
if (!Array.isArray(nodes)) continue;
|
|
987
|
+
const issue = worth[i];
|
|
988
|
+
const comments = nodes.filter((c) => c.author && !BOT_USERS.has(c.author.login)).filter((c) => !COMMENT_NOISE_RE.test((c.body || "").trim())).map((c) => {
|
|
989
|
+
const isMaintainer = [
|
|
990
|
+
"OWNER",
|
|
991
|
+
"MEMBER",
|
|
992
|
+
"COLLABORATOR"
|
|
993
|
+
].includes(c.authorAssociation);
|
|
994
|
+
const body = c.body || "";
|
|
995
|
+
const reactions = c.reactions?.totalCount || 0;
|
|
996
|
+
const _score = (isMaintainer ? 3 : 1) * (hasCodeBlock(body) ? 2 : 1) * (1 + reactions);
|
|
997
|
+
return {
|
|
998
|
+
body,
|
|
999
|
+
author: c.author.login,
|
|
1000
|
+
reactions,
|
|
1001
|
+
isMaintainer,
|
|
1002
|
+
_score
|
|
1003
|
+
};
|
|
1004
|
+
}).sort((a, b) => b._score - a._score);
|
|
1005
|
+
issue.topComments = comments.slice(0, 3).map(({ _score: _, ...c }) => c);
|
|
1006
|
+
if (issue.state === "closed") issue.resolvedIn = detectResolvedVersion(comments);
|
|
1007
|
+
}
|
|
1008
|
+
} catch {}
|
|
1009
|
+
}
|
|
1010
|
+
function detectResolvedVersion(comments) {
|
|
1011
|
+
const maintainerComments = comments.filter((c) => c.isMaintainer);
|
|
1012
|
+
for (const c of maintainerComments.reverse()) {
|
|
1013
|
+
const match = c.body.match(STATIC_REGEX_4);
|
|
1014
|
+
if (match) return match[1];
|
|
1015
|
+
if (c.body.length < 100) {
|
|
1016
|
+
const vMatch = c.body.match(STATIC_REGEX_5);
|
|
1017
|
+
if (vMatch) return vMatch[1];
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async function fetchGitHubIssues(owner, repo, limit = 30, releasedAt, fromDate) {
|
|
1022
|
+
if (!isGhAvailable()) return [];
|
|
1023
|
+
const openCount = Math.ceil(limit * .75);
|
|
1024
|
+
const closedCount = limit - openCount;
|
|
1025
|
+
try {
|
|
1026
|
+
const open = fetchIssuesByState(owner, repo, "open", Math.min(openCount * 2, 100), releasedAt, fromDate);
|
|
1027
|
+
const closed = fetchIssuesByState(owner, repo, "closed", Math.min(closedCount * 2, 50), releasedAt, fromDate);
|
|
1028
|
+
const selected = applyTypeQuotas([...open, ...closed], limit);
|
|
1029
|
+
enrichWithComments(owner, repo, selected);
|
|
1030
|
+
return selected;
|
|
1031
|
+
} catch {
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function formatIssueAsMarkdown(issue) {
|
|
1036
|
+
const limit = bodyLimit(issue.reactions);
|
|
1037
|
+
const fmFields = {
|
|
1038
|
+
number: issue.number,
|
|
1039
|
+
title: issue.title,
|
|
1040
|
+
type: issue.type,
|
|
1041
|
+
state: issue.state,
|
|
1042
|
+
created: isoDate(issue.createdAt),
|
|
1043
|
+
url: issue.url,
|
|
1044
|
+
reactions: issue.reactions,
|
|
1045
|
+
comments: issue.comments
|
|
1046
|
+
};
|
|
1047
|
+
if (issue.resolvedIn) fmFields.resolvedIn = issue.resolvedIn;
|
|
1048
|
+
if (issue.labels.length > 0) fmFields.labels = `[${issue.labels.join(", ")}]`;
|
|
1049
|
+
const lines = [
|
|
1050
|
+
buildFrontmatter(fmFields),
|
|
1051
|
+
"",
|
|
1052
|
+
`# ${issue.title}`
|
|
1053
|
+
];
|
|
1054
|
+
if (issue.body) {
|
|
1055
|
+
const body = truncateBody(issue.body, limit);
|
|
1056
|
+
lines.push("", body);
|
|
1057
|
+
}
|
|
1058
|
+
if (issue.topComments.length > 0) {
|
|
1059
|
+
lines.push("", "---", "", "## Top Comments");
|
|
1060
|
+
for (const c of issue.topComments) {
|
|
1061
|
+
const reactions = c.reactions > 0 ? ` (+${c.reactions})` : "";
|
|
1062
|
+
const maintainer = c.isMaintainer ? " [maintainer]" : "";
|
|
1063
|
+
const commentBody = truncateBody(c.body, 600);
|
|
1064
|
+
lines.push("", `**@${c.author}**${maintainer}${reactions}:`, "", commentBody);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return lines.join("\n");
|
|
1068
|
+
}
|
|
1069
|
+
function generateIssueIndex(issues) {
|
|
1070
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1071
|
+
for (const issue of issues) mapInsert(byType, issue.type, () => []).push(issue);
|
|
1072
|
+
const typeLabels = {
|
|
1073
|
+
bug: "Bugs & Regressions",
|
|
1074
|
+
question: "Questions & Usage Help",
|
|
1075
|
+
docs: "Documentation",
|
|
1076
|
+
feature: "Feature Requests",
|
|
1077
|
+
other: "Other"
|
|
1078
|
+
};
|
|
1079
|
+
const typeOrder = [
|
|
1080
|
+
"bug",
|
|
1081
|
+
"question",
|
|
1082
|
+
"docs",
|
|
1083
|
+
"other",
|
|
1084
|
+
"feature"
|
|
1085
|
+
];
|
|
1086
|
+
const sections = [
|
|
1087
|
+
[
|
|
1088
|
+
"---",
|
|
1089
|
+
`total: ${issues.length}`,
|
|
1090
|
+
`open: ${issues.filter((i) => i.state === "open").length}`,
|
|
1091
|
+
`closed: ${issues.filter((i) => i.state !== "open").length}`,
|
|
1092
|
+
"---"
|
|
1093
|
+
].join("\n"),
|
|
1094
|
+
"",
|
|
1095
|
+
"# Issues Index",
|
|
1096
|
+
""
|
|
1097
|
+
];
|
|
1098
|
+
for (const type of typeOrder) {
|
|
1099
|
+
const group = byType.get(type);
|
|
1100
|
+
if (!group?.length) continue;
|
|
1101
|
+
sections.push(`## ${typeLabels[type]} (${group.length})`, "");
|
|
1102
|
+
for (const issue of group) {
|
|
1103
|
+
const reactions = issue.reactions > 0 ? ` (+${issue.reactions})` : "";
|
|
1104
|
+
const state = issue.state === "open" ? "" : " [closed]";
|
|
1105
|
+
const resolved = issue.resolvedIn ? ` [fixed in ${issue.resolvedIn}]` : "";
|
|
1106
|
+
const date = isoDate(issue.createdAt);
|
|
1107
|
+
sections.push(`- [#${issue.number}](./issue-${issue.number}.md): ${issue.title}${reactions}${state}${resolved} (${date})`);
|
|
1108
|
+
}
|
|
1109
|
+
sections.push("");
|
|
1110
|
+
}
|
|
1111
|
+
return sections.join("\n");
|
|
1112
|
+
}
|
|
1113
|
+
async function fetchLlmsUrl(docsUrl) {
|
|
1114
|
+
const llmsUrl = `${new URL(docsUrl).origin}/llms.txt`;
|
|
1115
|
+
if (await verifyUrl(llmsUrl)) return llmsUrl;
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
async function fetchLlmsTxt(url) {
|
|
1119
|
+
const content = await fetchText(url);
|
|
1120
|
+
if (!content || content.length < 50) return null;
|
|
1108
1121
|
return {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
files: docs,
|
|
1112
|
-
docsPrefix,
|
|
1113
|
-
allFiles,
|
|
1114
|
-
fallback: tag.fallback
|
|
1122
|
+
raw: content,
|
|
1123
|
+
links: parseMarkdownLinks(content)
|
|
1115
1124
|
};
|
|
1116
1125
|
}
|
|
1117
|
-
function
|
|
1118
|
-
return
|
|
1126
|
+
function parseMarkdownLinks(content) {
|
|
1127
|
+
return extractLinks(content).filter((l) => l.url.endsWith(".md"));
|
|
1119
1128
|
}
|
|
1120
|
-
function
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
if (
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1129
|
+
async function downloadLlmsDocs(llmsContent, baseUrl, onProgress) {
|
|
1130
|
+
const limit = pLimit(5);
|
|
1131
|
+
let completed = 0;
|
|
1132
|
+
return (await Promise.all(llmsContent.links.map((link) => limit(async () => {
|
|
1133
|
+
const url = link.url.startsWith("http") ? link.url : `${baseUrl.replace(TRAILING_SLASH_RE, "")}${link.url.startsWith("/") ? "" : "/"}${link.url}`;
|
|
1134
|
+
if (!isSafeUrl(url)) return null;
|
|
1135
|
+
const content = await fetchText(url);
|
|
1136
|
+
onProgress?.(link.url, ++completed, llmsContent.links.length);
|
|
1137
|
+
if (content && content.length > 100) return {
|
|
1138
|
+
url: link.url.startsWith("http") ? new URL(link.url).pathname : link.url,
|
|
1139
|
+
title: link.title,
|
|
1140
|
+
content
|
|
1141
|
+
};
|
|
1142
|
+
return null;
|
|
1143
|
+
})))).filter((d) => d !== null);
|
|
1144
|
+
}
|
|
1145
|
+
function normalizeLlmsLinks(content, baseUrl) {
|
|
1146
|
+
let normalized = content;
|
|
1147
|
+
if (baseUrl) {
|
|
1148
|
+
const escaped = baseUrl.replace(TRAILING_SLASH_RE, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1149
|
+
normalized = normalized.replace(new RegExp(`\\]\\(${escaped}(/[^)]+\\.md)\\)`, "g"), "](./docs$1)");
|
|
1138
1150
|
}
|
|
1139
|
-
|
|
1140
|
-
return
|
|
1141
|
-
isValid: matchRatio >= .3,
|
|
1142
|
-
matchRatio
|
|
1143
|
-
};
|
|
1151
|
+
normalized = normalized.replace(/\]\(\/([^)]+\.md)\)/g, "](./docs/$1)");
|
|
1152
|
+
return normalized;
|
|
1144
1153
|
}
|
|
1145
1154
|
async function verifyNpmRepo(owner, repo, packageName) {
|
|
1146
1155
|
const base = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD`;
|
|
1147
1156
|
const paths = [
|
|
1148
1157
|
"package.json",
|
|
1149
|
-
`packages/${packageName.replace(
|
|
1150
|
-
`packages/${packageName.replace(
|
|
1158
|
+
`packages/${packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "")}/package.json`,
|
|
1159
|
+
`packages/${packageName.replace(NPM_SCOPE_PREFIX_RE, "").replace("/", "-")}/package.json`
|
|
1151
1160
|
];
|
|
1152
1161
|
for (const path of paths) {
|
|
1153
1162
|
const text = await fetchGitHubRaw(`${base}/${path}`);
|
|
@@ -1159,15 +1168,15 @@ async function verifyNpmRepo(owner, repo, packageName) {
|
|
|
1159
1168
|
return false;
|
|
1160
1169
|
}
|
|
1161
1170
|
async function searchGitHubRepo(packageName) {
|
|
1162
|
-
const shortName = packageName.replace(
|
|
1163
|
-
for (const candidate of [packageName.replace(
|
|
1171
|
+
const shortName = packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "");
|
|
1172
|
+
for (const candidate of [packageName.replace(NPM_SCOPE_PREFIX_RE, "").replace("/", "/"), shortName]) {
|
|
1164
1173
|
if (!candidate.includes("/")) {
|
|
1165
1174
|
if ((await $fetch.raw(`https://ungh.cc/repos/${shortName}/${shortName}`).catch(() => null))?.ok) return `https://github.com/${shortName}/${shortName}`;
|
|
1166
1175
|
continue;
|
|
1167
1176
|
}
|
|
1168
1177
|
if ((await $fetch.raw(`https://ungh.cc/repos/${candidate}`).catch(() => null))?.ok) return `https://github.com/${candidate}`;
|
|
1169
1178
|
}
|
|
1170
|
-
const searchTerm = packageName.replace(
|
|
1179
|
+
const searchTerm = packageName.replace(NPM_SCOPE_PREFIX_RE, "");
|
|
1171
1180
|
if (isGhAvailable()) try {
|
|
1172
1181
|
const { stdout: json } = spawnSync("gh", [
|
|
1173
1182
|
"search",
|
|
@@ -1274,7 +1283,7 @@ async function resolveGitHubRepo(owner, repo, onProgress) {
|
|
|
1274
1283
|
let releasedAt;
|
|
1275
1284
|
const latestRelease = releases[0];
|
|
1276
1285
|
if (latestRelease) {
|
|
1277
|
-
version = latestRelease.tag.replace(
|
|
1286
|
+
version = latestRelease.tag.replace(V_PREFIX_RE, "");
|
|
1278
1287
|
releasedAt = latestRelease.publishedAt;
|
|
1279
1288
|
}
|
|
1280
1289
|
onProgress?.("Resolving docs");
|
|
@@ -1453,6 +1462,8 @@ async function resolveCrateDocsWithAttempts(crateName, options = {}) {
|
|
|
1453
1462
|
attempts
|
|
1454
1463
|
};
|
|
1455
1464
|
}
|
|
1465
|
+
const STATIC_REGEX_2 = /[_.:-]/;
|
|
1466
|
+
const STATIC_REGEX_3 = /\/+$/;
|
|
1456
1467
|
async function fetchCrawledDocs(url, onProgress, maxPages = 200) {
|
|
1457
1468
|
const outputDir = join(tmpdir(), "skilld-crawl", Date.now().toString());
|
|
1458
1469
|
onProgress?.(`Crawling ${url}`);
|
|
@@ -1492,7 +1503,7 @@ async function fetchCrawledDocs(url, onProgress, maxPages = 200) {
|
|
|
1492
1503
|
localeFiltered++;
|
|
1493
1504
|
continue;
|
|
1494
1505
|
}
|
|
1495
|
-
const segments = (new URL(result.url).pathname.replace(
|
|
1506
|
+
const segments = (new URL(result.url).pathname.replace(TRAILING_SLASH_RE, "") || "/index").split("/").filter(Boolean);
|
|
1496
1507
|
if (isForeignPathPrefix(segments[0], userLang)) {
|
|
1497
1508
|
localeFiltered++;
|
|
1498
1509
|
continue;
|
|
@@ -1540,11 +1551,11 @@ function isForeignPathPrefix(segment, userLang) {
|
|
|
1540
1551
|
return LOCALE_CODES.has(lower);
|
|
1541
1552
|
}
|
|
1542
1553
|
function getUserLang() {
|
|
1543
|
-
const code = (process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || "").split(
|
|
1554
|
+
const code = (process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || "").split(STATIC_REGEX_2)[0]?.toLowerCase() || "";
|
|
1544
1555
|
return code.length >= 2 ? code.slice(0, 2) : "en";
|
|
1545
1556
|
}
|
|
1546
1557
|
function toCrawlPattern(docsUrl) {
|
|
1547
|
-
return `${docsUrl.replace(
|
|
1558
|
+
return `${docsUrl.replace(STATIC_REGEX_3, "")}/**`;
|
|
1548
1559
|
}
|
|
1549
1560
|
const HIGH_VALUE_CATEGORIES = new Set([
|
|
1550
1561
|
"q&a",
|
|
@@ -1722,6 +1733,7 @@ function generateDiscussionIndex(discussions) {
|
|
|
1722
1733
|
}
|
|
1723
1734
|
return sections.join("\n");
|
|
1724
1735
|
}
|
|
1736
|
+
const STATIC_REGEX_1$4 = /\.md$/;
|
|
1725
1737
|
function generateDocsIndex(docs) {
|
|
1726
1738
|
const docFiles = docs.filter((d) => d.path.startsWith("docs/") && d.path.endsWith(".md") && !d.path.endsWith("_INDEX.md")).sort((a, b) => a.path.localeCompare(b.path));
|
|
1727
1739
|
if (docFiles.length === 0) return "";
|
|
@@ -1747,7 +1759,7 @@ function generateDocsIndex(docs) {
|
|
|
1747
1759
|
];
|
|
1748
1760
|
for (const file of rootFiles) {
|
|
1749
1761
|
const rel = file.path.slice(5);
|
|
1750
|
-
const title = extractTitle(file.content) || rel.replace(
|
|
1762
|
+
const title = extractTitle(file.content) || rel.replace(STATIC_REGEX_1$4, "");
|
|
1751
1763
|
const desc = extractDescription(file.content);
|
|
1752
1764
|
const descPart = desc ? `: ${desc}` : "";
|
|
1753
1765
|
sections.push(`- [${title}](./${rel})${descPart}`);
|
|
@@ -1757,7 +1769,7 @@ function generateDocsIndex(docs) {
|
|
|
1757
1769
|
sections.push(`## ${dir} (${files.length})`, "");
|
|
1758
1770
|
for (const file of files) {
|
|
1759
1771
|
const rel = file.path.slice(5);
|
|
1760
|
-
const title = extractTitle(file.content) || rel.replace(
|
|
1772
|
+
const title = extractTitle(file.content) || rel.replace(STATIC_REGEX_1$4, "").split("/").pop();
|
|
1761
1773
|
const desc = extractDescription(file.content);
|
|
1762
1774
|
const descPart = desc ? `: ${desc}` : "";
|
|
1763
1775
|
sections.push(`- [${title}](./${rel})${descPart}`);
|
|
@@ -1795,12 +1807,24 @@ const SKIP_PATTERNS = [
|
|
|
1795
1807
|
const MAX_FILE_SIZE = 500 * 1024;
|
|
1796
1808
|
async function resolveEntryFiles(packageDir) {
|
|
1797
1809
|
if (!existsSync(join(packageDir, "package.json"))) return [];
|
|
1798
|
-
const
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1810
|
+
const skipDirSet = new Set(SKIP_DIRS);
|
|
1811
|
+
const isSkipPattern = (name) => SKIP_PATTERNS.some((p) => {
|
|
1812
|
+
const star = p.indexOf("*");
|
|
1813
|
+
if (star === -1) return name === p;
|
|
1814
|
+
const prefix = p.slice(0, star);
|
|
1815
|
+
const suffix = p.slice(star + 1);
|
|
1816
|
+
return name.startsWith(prefix) && name.endsWith(suffix);
|
|
1803
1817
|
});
|
|
1818
|
+
const files = [];
|
|
1819
|
+
for await (const file of glob(["**/*.d.{ts,mts,cts}"], {
|
|
1820
|
+
cwd: packageDir,
|
|
1821
|
+
exclude: (p) => {
|
|
1822
|
+
const segs = p.split("/");
|
|
1823
|
+
const last = segs[segs.length - 1];
|
|
1824
|
+
if (isSkipPattern(last)) return true;
|
|
1825
|
+
return segs.some((s) => skipDirSet.has(s));
|
|
1826
|
+
}
|
|
1827
|
+
})) files.push(file);
|
|
1804
1828
|
const entries = [];
|
|
1805
1829
|
for (const file of files) {
|
|
1806
1830
|
const absPath = join(packageDir, file);
|
|
@@ -1819,6 +1843,7 @@ async function resolveEntryFiles(packageDir) {
|
|
|
1819
1843
|
}
|
|
1820
1844
|
return entries;
|
|
1821
1845
|
}
|
|
1846
|
+
const STATIC_REGEX_1$3 = /^[\w.-]+\/[\w.-]+$/;
|
|
1822
1847
|
function parseGitSkillInput(input) {
|
|
1823
1848
|
const trimmed = input.trim();
|
|
1824
1849
|
if (trimmed.startsWith("@")) return null;
|
|
@@ -1836,7 +1861,7 @@ function parseGitSkillInput(input) {
|
|
|
1836
1861
|
return null;
|
|
1837
1862
|
}
|
|
1838
1863
|
if (trimmed.startsWith("https://") || trimmed.startsWith("http://")) return parseGitUrl(trimmed);
|
|
1839
|
-
if (
|
|
1864
|
+
if (STATIC_REGEX_1$3.test(trimmed)) return {
|
|
1840
1865
|
type: "github",
|
|
1841
1866
|
owner: trimmed.split("/")[0],
|
|
1842
1867
|
repo: trimmed.split("/")[1]
|
|
@@ -1847,7 +1872,7 @@ function parseGitUrl(url) {
|
|
|
1847
1872
|
try {
|
|
1848
1873
|
const parsed = new URL(url);
|
|
1849
1874
|
if (parsed.hostname === "github.com" || parsed.hostname === "www.github.com") {
|
|
1850
|
-
const parts = parsed.pathname.replace(
|
|
1875
|
+
const parts = parsed.pathname.replace(LEADING_SLASH_RE, "").replace(GIT_SUFFIX_RE, "").split("/");
|
|
1851
1876
|
const owner = parts[0];
|
|
1852
1877
|
const repo = parts[1];
|
|
1853
1878
|
if (!owner || !repo) return null;
|
|
@@ -1865,7 +1890,7 @@ function parseGitUrl(url) {
|
|
|
1865
1890
|
};
|
|
1866
1891
|
}
|
|
1867
1892
|
if (parsed.hostname === "gitlab.com") {
|
|
1868
|
-
const parts = parsed.pathname.replace(
|
|
1893
|
+
const parts = parsed.pathname.replace(LEADING_SLASH_RE, "").replace(GIT_SUFFIX_RE, "").split("/");
|
|
1869
1894
|
const owner = parts[0];
|
|
1870
1895
|
const repo = parts[1];
|
|
1871
1896
|
if (!owner || !repo) return null;
|
|
@@ -2004,287 +2029,65 @@ async function downloadGitHubSkills(owner, repo, ref, skillPath, onProgress) {
|
|
|
2004
2029
|
path: "",
|
|
2005
2030
|
content,
|
|
2006
2031
|
files: []
|
|
2007
|
-
}];
|
|
2008
|
-
}
|
|
2009
|
-
return [];
|
|
2010
|
-
} catch {
|
|
2011
|
-
return [];
|
|
2012
|
-
} finally {
|
|
2013
|
-
rmSync(tempDir, {
|
|
2014
|
-
recursive: true,
|
|
2015
|
-
force: true
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
async function fetchGitLabSkills(source, onProgress) {
|
|
2020
|
-
const { owner, repo } = source;
|
|
2021
|
-
if (!owner || !repo) return { skills: [] };
|
|
2022
|
-
const ref = source.ref || "main";
|
|
2023
|
-
const tempDir = join(tmpdir(), `skilld-gitlab-${Date.now()}`);
|
|
2024
|
-
try {
|
|
2025
|
-
const subdir = source.skillPath || "skills";
|
|
2026
|
-
onProgress?.(`Downloading ${owner}/${repo}/${subdir}@${ref}`);
|
|
2027
|
-
const { dir } = await downloadTemplate(`gitlab:${owner}/${repo}/${subdir}#${ref}`, {
|
|
2028
|
-
dir: tempDir,
|
|
2029
|
-
force: true
|
|
2030
|
-
});
|
|
2031
|
-
if (source.skillPath) {
|
|
2032
|
-
const skill = readLocalSkill(dir, source.skillPath);
|
|
2033
|
-
return { skills: skill ? [skill] : [] };
|
|
2034
|
-
}
|
|
2035
|
-
const skills = [];
|
|
2036
|
-
for (const { dir: skillDir, repoPath } of findSkillDirs(dir, "skills")) {
|
|
2037
|
-
const skill = readLocalSkill(skillDir, repoPath);
|
|
2038
|
-
if (skill) skills.push(skill);
|
|
2039
|
-
}
|
|
2040
|
-
if (skills.length > 0) {
|
|
2041
|
-
onProgress?.(`Found ${skills.length} skill(s)`);
|
|
2042
|
-
return { skills };
|
|
2043
|
-
}
|
|
2044
|
-
const content = await $fetch(`https://gitlab.com/${owner}/${repo}/-/raw/${ref}/SKILL.md`, { responseType: "text" }).catch(() => null);
|
|
2045
|
-
if (content) {
|
|
2046
|
-
const fm = parseSkillFrontmatterName(content);
|
|
2047
|
-
return { skills: [{
|
|
2048
|
-
name: fm.name || repo,
|
|
2049
|
-
description: fm.description || "",
|
|
2050
|
-
path: "",
|
|
2051
|
-
content,
|
|
2052
|
-
files: []
|
|
2053
|
-
}] };
|
|
2054
|
-
}
|
|
2055
|
-
return { skills: [] };
|
|
2056
|
-
} catch {
|
|
2057
|
-
return { skills: [] };
|
|
2058
|
-
} finally {
|
|
2059
|
-
rmSync(tempDir, {
|
|
2060
|
-
recursive: true,
|
|
2061
|
-
force: true
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
async function searchNpmPackages(query, size = 5) {
|
|
2066
|
-
const data = await $fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`).catch(() => null);
|
|
2067
|
-
if (!data?.objects?.length) return [];
|
|
2068
|
-
return data.objects.map((o) => ({
|
|
2069
|
-
name: o.package.name,
|
|
2070
|
-
description: o.package.description,
|
|
2071
|
-
version: o.package.version
|
|
2072
|
-
}));
|
|
2073
|
-
}
|
|
2074
|
-
async function fetchNpmPackage(packageName) {
|
|
2075
|
-
const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
|
|
2076
|
-
if (data) return data;
|
|
2077
|
-
return $fetch(`https://registry.npmjs.org/${packageName}/latest`).catch(() => null);
|
|
2078
|
-
}
|
|
2079
|
-
async function fetchNpmRegistryMeta(packageName, version) {
|
|
2080
|
-
const { name: barePackageName } = parsePackageSpec(packageName);
|
|
2081
|
-
const data = await $fetch(`https://registry.npmjs.org/${barePackageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null);
|
|
2082
|
-
if (!data) return {};
|
|
2083
|
-
const distTags = data["dist-tags"] ? Object.fromEntries(Object.entries(data["dist-tags"]).map(([tag, ver]) => [tag, {
|
|
2084
|
-
version: ver,
|
|
2085
|
-
releasedAt: data.time?.[ver]
|
|
2086
|
-
}])) : void 0;
|
|
2087
|
-
return {
|
|
2088
|
-
releasedAt: data.time?.[version] || void 0,
|
|
2089
|
-
distTags
|
|
2090
|
-
};
|
|
2091
|
-
}
|
|
2092
|
-
async function resolveGitHub(gh, targetVersion, pkg, result, attempts, onProgress, opts) {
|
|
2093
|
-
let allFiles;
|
|
2094
|
-
if (targetVersion) {
|
|
2095
|
-
onProgress?.("github-docs");
|
|
2096
|
-
const gitDocs = await fetchGitDocs(gh.owner, gh.repo, targetVersion, pkg.name, opts?.rawRepoUrl);
|
|
2097
|
-
if (gitDocs) {
|
|
2098
|
-
result.gitDocsUrl = gitDocs.baseUrl;
|
|
2099
|
-
result.gitRef = gitDocs.ref;
|
|
2100
|
-
result.gitDocsFallback = gitDocs.fallback;
|
|
2101
|
-
allFiles = gitDocs.allFiles;
|
|
2102
|
-
attempts.push({
|
|
2103
|
-
source: "github-docs",
|
|
2104
|
-
url: gitDocs.baseUrl,
|
|
2105
|
-
status: "success",
|
|
2106
|
-
message: gitDocs.fallback ? `Found ${gitDocs.files.length} docs at ${gitDocs.ref} (no tag for v${targetVersion})` : `Found ${gitDocs.files.length} docs at ${gitDocs.ref}`
|
|
2107
|
-
});
|
|
2108
|
-
} else attempts.push({
|
|
2109
|
-
source: "github-docs",
|
|
2110
|
-
url: `${result.repoUrl}/tree/v${targetVersion}/docs`,
|
|
2111
|
-
status: "not-found",
|
|
2112
|
-
message: "No docs/ folder found at version tag"
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
if (!result.docsUrl) {
|
|
2116
|
-
onProgress?.("github-meta");
|
|
2117
|
-
const repoMeta = await fetchGitHubRepoMeta(gh.owner, gh.repo, pkg.name);
|
|
2118
|
-
if (repoMeta?.homepage && !isUselessDocsUrl(repoMeta.homepage)) {
|
|
2119
|
-
result.docsUrl = repoMeta.homepage;
|
|
2120
|
-
attempts.push({
|
|
2121
|
-
source: "github-meta",
|
|
2122
|
-
url: result.repoUrl,
|
|
2123
|
-
status: "success",
|
|
2124
|
-
message: `Found homepage: ${repoMeta.homepage}`
|
|
2125
|
-
});
|
|
2126
|
-
} else attempts.push({
|
|
2127
|
-
source: "github-meta",
|
|
2128
|
-
url: result.repoUrl,
|
|
2129
|
-
status: "not-found",
|
|
2130
|
-
message: "No homepage in repo metadata"
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
onProgress?.("readme");
|
|
2134
|
-
const readmeUrl = await fetchReadme(gh.owner, gh.repo, opts?.subdir, result.gitRef);
|
|
2135
|
-
if (readmeUrl) {
|
|
2136
|
-
result.readmeUrl = readmeUrl;
|
|
2137
|
-
attempts.push({
|
|
2138
|
-
source: "readme",
|
|
2139
|
-
url: readmeUrl,
|
|
2140
|
-
status: "success"
|
|
2141
|
-
});
|
|
2142
|
-
} else attempts.push({
|
|
2143
|
-
source: "readme",
|
|
2144
|
-
url: `${result.repoUrl}/README.md`,
|
|
2145
|
-
status: "not-found",
|
|
2146
|
-
message: "No README found"
|
|
2147
|
-
});
|
|
2148
|
-
return allFiles;
|
|
2149
|
-
}
|
|
2150
|
-
async function resolvePackageDocs(packageName, options = {}) {
|
|
2151
|
-
return (await resolvePackageDocsWithAttempts(packageName, options)).package;
|
|
2152
|
-
}
|
|
2153
|
-
async function resolvePackageDocsWithAttempts(packageName, options = {}) {
|
|
2154
|
-
const attempts = [];
|
|
2155
|
-
const { onProgress } = options;
|
|
2156
|
-
onProgress?.("npm");
|
|
2157
|
-
const pkg = await fetchNpmPackage(packageName);
|
|
2158
|
-
if (!pkg) {
|
|
2159
|
-
attempts.push({
|
|
2160
|
-
source: "npm",
|
|
2161
|
-
url: `https://registry.npmjs.org/${packageName}/latest`,
|
|
2162
|
-
status: "not-found",
|
|
2163
|
-
message: "Package not found on npm registry"
|
|
2164
|
-
});
|
|
2165
|
-
return {
|
|
2166
|
-
package: null,
|
|
2167
|
-
attempts
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
attempts.push({
|
|
2171
|
-
source: "npm",
|
|
2172
|
-
url: `https://registry.npmjs.org/${packageName}/latest`,
|
|
2173
|
-
status: "success",
|
|
2174
|
-
message: `Found ${pkg.name}@${pkg.version}`
|
|
2175
|
-
});
|
|
2176
|
-
const registryMeta = pkg.version ? await fetchNpmRegistryMeta(packageName, pkg.version) : {};
|
|
2177
|
-
const result = {
|
|
2178
|
-
name: pkg.name,
|
|
2179
|
-
version: pkg.version,
|
|
2180
|
-
releasedAt: registryMeta.releasedAt,
|
|
2181
|
-
description: pkg.description,
|
|
2182
|
-
dependencies: pkg.dependencies,
|
|
2183
|
-
distTags: registryMeta.distTags
|
|
2184
|
-
};
|
|
2185
|
-
let gitDocsAllFiles;
|
|
2186
|
-
let subdir;
|
|
2187
|
-
let rawRepoUrl;
|
|
2188
|
-
if (typeof pkg.repository === "object" && pkg.repository?.url) {
|
|
2189
|
-
rawRepoUrl = pkg.repository.url;
|
|
2190
|
-
const normalized = normalizeRepoUrl(rawRepoUrl);
|
|
2191
|
-
if (!normalized.includes("://") && normalized.includes("/") && !normalized.includes(":")) result.repoUrl = `https://github.com/${normalized}`;
|
|
2192
|
-
else result.repoUrl = normalized;
|
|
2193
|
-
subdir = pkg.repository.directory;
|
|
2194
|
-
} else if (typeof pkg.repository === "string") if (pkg.repository.includes("://")) {
|
|
2195
|
-
const gh = parseGitHubUrl(pkg.repository);
|
|
2196
|
-
if (gh) result.repoUrl = `https://github.com/${gh.owner}/${gh.repo}`;
|
|
2197
|
-
} else {
|
|
2198
|
-
const repo = pkg.repository.replace(/^github:/, "");
|
|
2199
|
-
if (repo.includes("/") && !repo.includes(":")) result.repoUrl = `https://github.com/${repo}`;
|
|
2200
|
-
}
|
|
2201
|
-
if (pkg.homepage && !isGitHubRepoUrl(pkg.homepage) && !isUselessDocsUrl(pkg.homepage)) result.docsUrl = pkg.homepage;
|
|
2202
|
-
if (result.repoUrl?.includes("github.com")) {
|
|
2203
|
-
const gh = parseGitHubUrl(result.repoUrl);
|
|
2204
|
-
if (gh) gitDocsAllFiles = await resolveGitHub(gh, options.version || pkg.version, pkg, result, attempts, onProgress, {
|
|
2205
|
-
rawRepoUrl,
|
|
2206
|
-
subdir
|
|
2207
|
-
});
|
|
2208
|
-
} else if (!result.repoUrl) {
|
|
2209
|
-
onProgress?.("github-search");
|
|
2210
|
-
const searchedUrl = await searchGitHubRepo(pkg.name);
|
|
2211
|
-
if (searchedUrl) {
|
|
2212
|
-
result.repoUrl = searchedUrl;
|
|
2213
|
-
attempts.push({
|
|
2214
|
-
source: "github-search",
|
|
2215
|
-
url: searchedUrl,
|
|
2216
|
-
status: "success",
|
|
2217
|
-
message: `Found via GitHub search: ${searchedUrl}`
|
|
2218
|
-
});
|
|
2219
|
-
const gh = parseGitHubUrl(searchedUrl);
|
|
2220
|
-
if (gh) gitDocsAllFiles = await resolveGitHub(gh, options.version || pkg.version, pkg, result, attempts, onProgress);
|
|
2221
|
-
} else attempts.push({
|
|
2222
|
-
source: "github-search",
|
|
2223
|
-
status: "not-found",
|
|
2224
|
-
message: "No repository URL in package.json and GitHub search found no match"
|
|
2225
|
-
});
|
|
2226
|
-
}
|
|
2227
|
-
const crawlUrl = getCrawlUrl(packageName);
|
|
2228
|
-
if (crawlUrl) result.crawlUrl = crawlUrl;
|
|
2229
|
-
if (result.docsUrl) {
|
|
2230
|
-
onProgress?.("llms.txt");
|
|
2231
|
-
const llmsUrl = await fetchLlmsUrl(result.docsUrl);
|
|
2232
|
-
if (llmsUrl) {
|
|
2233
|
-
result.llmsUrl = llmsUrl;
|
|
2234
|
-
attempts.push({
|
|
2235
|
-
source: "llms.txt",
|
|
2236
|
-
url: llmsUrl,
|
|
2237
|
-
status: "success"
|
|
2238
|
-
});
|
|
2239
|
-
} else attempts.push({
|
|
2240
|
-
source: "llms.txt",
|
|
2241
|
-
url: `${new URL(result.docsUrl).origin}/llms.txt`,
|
|
2242
|
-
status: "not-found",
|
|
2243
|
-
message: "No llms.txt at docs URL"
|
|
2032
|
+
}];
|
|
2033
|
+
}
|
|
2034
|
+
return [];
|
|
2035
|
+
} catch {
|
|
2036
|
+
return [];
|
|
2037
|
+
} finally {
|
|
2038
|
+
rmSync(tempDir, {
|
|
2039
|
+
recursive: true,
|
|
2040
|
+
force: true
|
|
2244
2041
|
});
|
|
2245
2042
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2043
|
+
}
|
|
2044
|
+
async function fetchGitLabSkills(source, onProgress) {
|
|
2045
|
+
const { owner, repo } = source;
|
|
2046
|
+
if (!owner || !repo) return { skills: [] };
|
|
2047
|
+
const ref = source.ref || "main";
|
|
2048
|
+
const tempDir = join(tmpdir(), `skilld-gitlab-${Date.now()}`);
|
|
2049
|
+
try {
|
|
2050
|
+
const subdir = source.skillPath || "skills";
|
|
2051
|
+
onProgress?.(`Downloading ${owner}/${repo}/${subdir}@${ref}`);
|
|
2052
|
+
const { dir } = await downloadTemplate(`gitlab:${owner}/${repo}/${subdir}#${ref}`, {
|
|
2053
|
+
dir: tempDir,
|
|
2054
|
+
force: true
|
|
2055
|
+
});
|
|
2056
|
+
if (source.skillPath) {
|
|
2057
|
+
const skill = readLocalSkill(dir, source.skillPath);
|
|
2058
|
+
return { skills: skill ? [skill] : [] };
|
|
2260
2059
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
if (
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2060
|
+
const skills = [];
|
|
2061
|
+
for (const { dir: skillDir, repoPath } of findSkillDirs(dir, "skills")) {
|
|
2062
|
+
const skill = readLocalSkill(skillDir, repoPath);
|
|
2063
|
+
if (skill) skills.push(skill);
|
|
2064
|
+
}
|
|
2065
|
+
if (skills.length > 0) {
|
|
2066
|
+
onProgress?.(`Found ${skills.length} skill(s)`);
|
|
2067
|
+
return { skills };
|
|
2068
|
+
}
|
|
2069
|
+
const content = await $fetch(`https://gitlab.com/${owner}/${repo}/-/raw/${ref}/SKILL.md`, { responseType: "text" }).catch(() => null);
|
|
2070
|
+
if (content) {
|
|
2071
|
+
const fm = parseSkillFrontmatterName(content);
|
|
2072
|
+
return { skills: [{
|
|
2073
|
+
name: fm.name || repo,
|
|
2074
|
+
description: fm.description || "",
|
|
2075
|
+
path: "",
|
|
2076
|
+
content,
|
|
2077
|
+
files: []
|
|
2078
|
+
}] };
|
|
2275
2079
|
}
|
|
2080
|
+
return { skills: [] };
|
|
2081
|
+
} catch {
|
|
2082
|
+
return { skills: [] };
|
|
2083
|
+
} finally {
|
|
2084
|
+
rmSync(tempDir, {
|
|
2085
|
+
recursive: true,
|
|
2086
|
+
force: true
|
|
2087
|
+
});
|
|
2276
2088
|
}
|
|
2277
|
-
if (!result.docsUrl && !result.llmsUrl && !result.readmeUrl && !result.gitDocsUrl) return {
|
|
2278
|
-
package: null,
|
|
2279
|
-
attempts,
|
|
2280
|
-
registryVersion: pkg.version
|
|
2281
|
-
};
|
|
2282
|
-
return {
|
|
2283
|
-
package: result,
|
|
2284
|
-
attempts,
|
|
2285
|
-
registryVersion: pkg.version
|
|
2286
|
-
};
|
|
2287
2089
|
}
|
|
2090
|
+
const STATIC_REGEX_1$2 = /^[\^~>=<\d]/;
|
|
2288
2091
|
function parseVersionSpecifier(name, version, cwd) {
|
|
2289
2092
|
if (version.startsWith("link:")) {
|
|
2290
2093
|
const linkedPkg = readPackageJsonSafe(join(resolve(cwd, version.slice(5)), "package.json"));
|
|
@@ -2309,9 +2112,9 @@ function parseVersionSpecifier(name, version, cwd) {
|
|
|
2309
2112
|
name,
|
|
2310
2113
|
version: installed
|
|
2311
2114
|
};
|
|
2312
|
-
if (
|
|
2115
|
+
if (STATIC_REGEX_1$2.test(version)) return {
|
|
2313
2116
|
name,
|
|
2314
|
-
version: version.replace(
|
|
2117
|
+
version: version.replace(VERSION_RANGE_PREFIX_RE, "")
|
|
2315
2118
|
};
|
|
2316
2119
|
if (version.startsWith("catalog:") || version.startsWith("workspace:")) return {
|
|
2317
2120
|
name,
|
|
@@ -2320,11 +2123,14 @@ function parseVersionSpecifier(name, version, cwd) {
|
|
|
2320
2123
|
return null;
|
|
2321
2124
|
}
|
|
2322
2125
|
function resolveInstalledVersion(name, cwd) {
|
|
2126
|
+
const direct = readPackageJsonSafe(join(cwd, "node_modules", ...name.split("/"), "package.json"));
|
|
2127
|
+
if (direct) return direct.parsed.version || null;
|
|
2128
|
+
const req = createRequire(join(cwd, "package.json"));
|
|
2323
2129
|
try {
|
|
2324
|
-
return readPackageJsonSafe(
|
|
2130
|
+
return readPackageJsonSafe(req.resolve(`${name}/package.json`))?.parsed.version || null;
|
|
2325
2131
|
} catch {
|
|
2326
2132
|
try {
|
|
2327
|
-
let dir = dirname(
|
|
2133
|
+
let dir = dirname(req.resolve(name));
|
|
2328
2134
|
while (dir && basename(dir) !== "node_modules") {
|
|
2329
2135
|
const pkg = readPackageJsonSafe(join(dir, "package.json"));
|
|
2330
2136
|
if (pkg) return pkg.parsed.version || null;
|
|
@@ -2389,12 +2195,50 @@ async function resolveLocalPackageDocs(localPath) {
|
|
|
2389
2195
|
}
|
|
2390
2196
|
}
|
|
2391
2197
|
if (!result.readmeUrl && !result.gitDocsUrl) {
|
|
2392
|
-
const readmeFile = readdirSync(localPath).find((f) =>
|
|
2198
|
+
const readmeFile = readdirSync(localPath).find((f) => README_FILENAME_RE.test(f));
|
|
2393
2199
|
if (readmeFile) result.readmeUrl = pathToFileURL(join(localPath, readmeFile)).href;
|
|
2394
2200
|
}
|
|
2395
2201
|
if (!result.readmeUrl && !result.gitDocsUrl) return null;
|
|
2396
2202
|
return result;
|
|
2397
2203
|
}
|
|
2204
|
+
async function resolveLocalDep(packageName, cwd) {
|
|
2205
|
+
const result = readPackageJsonSafe(join(cwd, "package.json"));
|
|
2206
|
+
if (!result) return null;
|
|
2207
|
+
const pkg = result.parsed;
|
|
2208
|
+
const depVersion = {
|
|
2209
|
+
...pkg.dependencies,
|
|
2210
|
+
...pkg.devDependencies
|
|
2211
|
+
}[packageName];
|
|
2212
|
+
if (!depVersion?.startsWith("link:")) return null;
|
|
2213
|
+
return resolveLocalPackageDocs(resolve(cwd, depVersion.slice(5)));
|
|
2214
|
+
}
|
|
2215
|
+
async function searchNpmPackages(query, size = 5) {
|
|
2216
|
+
const data = await $fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`).catch(() => null);
|
|
2217
|
+
if (!data?.objects?.length) return [];
|
|
2218
|
+
return data.objects.map((o) => ({
|
|
2219
|
+
name: o.package.name,
|
|
2220
|
+
description: o.package.description,
|
|
2221
|
+
version: o.package.version
|
|
2222
|
+
}));
|
|
2223
|
+
}
|
|
2224
|
+
async function fetchNpmPackage(packageName) {
|
|
2225
|
+
const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
|
|
2226
|
+
if (data) return data;
|
|
2227
|
+
return $fetch(`https://registry.npmjs.org/${packageName}/latest`).catch(() => null);
|
|
2228
|
+
}
|
|
2229
|
+
async function fetchNpmRegistryMeta(packageName, version) {
|
|
2230
|
+
const { name: barePackageName } = parsePackageSpec(packageName);
|
|
2231
|
+
const data = await $fetch(`https://registry.npmjs.org/${barePackageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null);
|
|
2232
|
+
if (!data) return {};
|
|
2233
|
+
const distTags = data["dist-tags"] ? Object.fromEntries(Object.entries(data["dist-tags"]).map(([tag, ver]) => [tag, {
|
|
2234
|
+
version: ver,
|
|
2235
|
+
releasedAt: data.time?.[ver]
|
|
2236
|
+
}])) : void 0;
|
|
2237
|
+
return {
|
|
2238
|
+
releasedAt: data.time?.[version] || void 0,
|
|
2239
|
+
distTags
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2398
2242
|
async function fetchPkgDist(name, version) {
|
|
2399
2243
|
const cacheDir = getCacheDir(name, version);
|
|
2400
2244
|
const pkgDir = join(cacheDir, "pkg");
|
|
@@ -2467,11 +2311,457 @@ async function fetchLatestVersion(packageName) {
|
|
|
2467
2311
|
if (data?.version) return data.version;
|
|
2468
2312
|
return (await $fetch(`https://registry.npmjs.org/${packageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null))?.["dist-tags"]?.latest || null;
|
|
2469
2313
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2314
|
+
const STATIC_REGEX_1$1 = /^[\w.-]+\/[\w.-]+/;
|
|
2315
|
+
function parseSkillInput(input) {
|
|
2316
|
+
const trimmed = input.trim();
|
|
2317
|
+
if (trimmed.startsWith("npm:")) {
|
|
2318
|
+
const { name, tag } = splitPackageTag(trimmed.slice(4));
|
|
2319
|
+
return {
|
|
2320
|
+
type: "npm",
|
|
2321
|
+
package: name,
|
|
2322
|
+
tag
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
if (trimmed.startsWith("crate:")) {
|
|
2326
|
+
const rest = trimmed.slice(6).trim();
|
|
2327
|
+
const atIdx = rest.indexOf("@");
|
|
2328
|
+
return {
|
|
2329
|
+
type: "crate",
|
|
2330
|
+
package: (atIdx === -1 ? rest : rest.slice(0, atIdx)).toLowerCase(),
|
|
2331
|
+
version: atIdx === -1 ? void 0 : rest.slice(atIdx + 1) || void 0
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
if (trimmed.startsWith("gh:") || trimmed.startsWith("github:")) {
|
|
2335
|
+
const rest = trimmed.startsWith("gh:") ? trimmed.slice(3) : trimmed.slice(7);
|
|
2336
|
+
const gitSource = parseGitSkillInput(rest);
|
|
2337
|
+
if (gitSource) return {
|
|
2338
|
+
type: "git",
|
|
2339
|
+
source: gitSource
|
|
2340
|
+
};
|
|
2341
|
+
if (STATIC_REGEX_1$1.test(rest)) {
|
|
2342
|
+
const [owner, repo] = rest.split("/");
|
|
2343
|
+
return {
|
|
2344
|
+
type: "git",
|
|
2345
|
+
source: {
|
|
2346
|
+
type: "github",
|
|
2347
|
+
owner,
|
|
2348
|
+
repo
|
|
2349
|
+
}
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
return {
|
|
2353
|
+
type: "bare",
|
|
2354
|
+
package: rest
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
if (trimmed.startsWith("@")) {
|
|
2358
|
+
const rest = trimmed.slice(1);
|
|
2359
|
+
if (rest.indexOf("/") === -1) return {
|
|
2360
|
+
type: "curator",
|
|
2361
|
+
handle: rest
|
|
2362
|
+
};
|
|
2363
|
+
const { name, tag } = splitPackageTag(trimmed);
|
|
2364
|
+
return {
|
|
2365
|
+
type: "bare",
|
|
2366
|
+
package: name,
|
|
2367
|
+
tag
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
const gitSource = parseGitSkillInput(trimmed);
|
|
2371
|
+
if (gitSource) return {
|
|
2372
|
+
type: "git",
|
|
2373
|
+
source: gitSource
|
|
2374
|
+
};
|
|
2375
|
+
const { name, tag } = splitPackageTag(trimmed);
|
|
2376
|
+
return {
|
|
2377
|
+
type: "bare",
|
|
2378
|
+
package: name,
|
|
2379
|
+
tag
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
function resolveSkillName(input) {
|
|
2383
|
+
const source = parseSkillInput(input);
|
|
2384
|
+
switch (source.type) {
|
|
2385
|
+
case "npm":
|
|
2386
|
+
case "bare": return source.package;
|
|
2387
|
+
case "crate": return `crate:${source.package}`;
|
|
2388
|
+
case "git":
|
|
2389
|
+
if (source.source.type === "github" && source.source.repo) return source.source.repo;
|
|
2390
|
+
return null;
|
|
2391
|
+
case "curator":
|
|
2392
|
+
case "collection": return null;
|
|
2393
|
+
default: throw new Error(`Unhandled SkillSource type: ${JSON.stringify(source)}`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function toStoragePackageName(identityName) {
|
|
2397
|
+
if (identityName.startsWith("crate:")) return `@skilld-crate/${identityName.slice(6)}`;
|
|
2398
|
+
return identityName;
|
|
2399
|
+
}
|
|
2400
|
+
function isCrateSpec(spec) {
|
|
2401
|
+
return spec.startsWith("crate:");
|
|
2402
|
+
}
|
|
2403
|
+
function toCrateIdentity(crateName) {
|
|
2404
|
+
return `crate:${crateName}`;
|
|
2405
|
+
}
|
|
2406
|
+
function splitPackageTag(spec) {
|
|
2407
|
+
if (spec.startsWith("@")) {
|
|
2408
|
+
const slashIdx = spec.indexOf("/");
|
|
2409
|
+
if (slashIdx !== -1) {
|
|
2410
|
+
const afterSlash = spec.indexOf("@", slashIdx);
|
|
2411
|
+
if (afterSlash !== -1) return {
|
|
2412
|
+
name: spec.slice(0, afterSlash),
|
|
2413
|
+
tag: spec.slice(afterSlash + 1) || void 0
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
return { name: spec };
|
|
2417
|
+
}
|
|
2418
|
+
const atIdx = spec.indexOf("@");
|
|
2419
|
+
if (atIdx !== -1) return {
|
|
2420
|
+
name: spec.slice(0, atIdx),
|
|
2421
|
+
tag: spec.slice(atIdx + 1) || void 0
|
|
2422
|
+
};
|
|
2423
|
+
return { name: spec };
|
|
2424
|
+
}
|
|
2425
|
+
const crawlUrlResolver = defineResolver({
|
|
2426
|
+
id: "crawl",
|
|
2427
|
+
canResolve: (ctx) => !!ctx.result,
|
|
2428
|
+
async run(ctx) {
|
|
2429
|
+
const crawlUrl = getCrawlUrl(ctx.packageName);
|
|
2430
|
+
if (crawlUrl) ctx.result.crawlUrl = crawlUrl;
|
|
2431
|
+
return { kind: "ok" };
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
const gitTagResolver = defineResolver({
|
|
2435
|
+
id: "github-docs",
|
|
2436
|
+
canResolve: (ctx) => !!ctx.result?.repoUrl?.includes("github.com"),
|
|
2437
|
+
async run(ctx) {
|
|
2438
|
+
const result = ctx.result;
|
|
2439
|
+
const gh = parseGitHubUrl(result.repoUrl);
|
|
2440
|
+
if (!gh) return { kind: "skip" };
|
|
2441
|
+
const targetVersion = ctx.options.version || ctx.npm?.version;
|
|
2442
|
+
if (!targetVersion) return { kind: "skip" };
|
|
2443
|
+
ctx.options.onProgress?.("github-docs");
|
|
2444
|
+
const gitDocs = await fetchGitDocs(gh.owner, gh.repo, targetVersion, ctx.packageName, ctx.rawRepoUrl);
|
|
2445
|
+
if (gitDocs) {
|
|
2446
|
+
result.gitDocsUrl = gitDocs.baseUrl;
|
|
2447
|
+
result.gitRef = gitDocs.ref;
|
|
2448
|
+
result.gitDocsFallback = gitDocs.fallback;
|
|
2449
|
+
ctx.gitDocsAllFiles = gitDocs.allFiles;
|
|
2450
|
+
ctx.attempts.push({
|
|
2451
|
+
source: "github-docs",
|
|
2452
|
+
url: gitDocs.baseUrl,
|
|
2453
|
+
status: "success",
|
|
2454
|
+
message: gitDocs.fallback ? `Found ${gitDocs.files.length} docs at ${gitDocs.ref} (no tag for v${targetVersion})` : `Found ${gitDocs.files.length} docs at ${gitDocs.ref}`
|
|
2455
|
+
});
|
|
2456
|
+
return { kind: "ok" };
|
|
2457
|
+
}
|
|
2458
|
+
ctx.attempts.push({
|
|
2459
|
+
source: "github-docs",
|
|
2460
|
+
url: `${result.repoUrl}/tree/v${targetVersion}/docs`,
|
|
2461
|
+
status: "not-found",
|
|
2462
|
+
message: "No docs/ folder found at version tag"
|
|
2463
|
+
});
|
|
2464
|
+
return { kind: "skip" };
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
const githubMetaResolver = defineResolver({
|
|
2468
|
+
id: "github-meta",
|
|
2469
|
+
canResolve: (ctx) => !!ctx.result?.repoUrl?.includes("github.com") && !ctx.result.docsUrl,
|
|
2470
|
+
async run(ctx) {
|
|
2471
|
+
const result = ctx.result;
|
|
2472
|
+
const gh = parseGitHubUrl(result.repoUrl);
|
|
2473
|
+
if (!gh) return { kind: "skip" };
|
|
2474
|
+
ctx.options.onProgress?.("github-meta");
|
|
2475
|
+
const repoMeta = await fetchGitHubRepoMeta(gh.owner, gh.repo, ctx.packageName);
|
|
2476
|
+
if (repoMeta?.homepage && !isUselessDocsUrl(repoMeta.homepage)) {
|
|
2477
|
+
result.docsUrl = repoMeta.homepage;
|
|
2478
|
+
ctx.attempts.push({
|
|
2479
|
+
source: "github-meta",
|
|
2480
|
+
url: result.repoUrl,
|
|
2481
|
+
status: "success",
|
|
2482
|
+
message: `Found homepage: ${repoMeta.homepage}`
|
|
2483
|
+
});
|
|
2484
|
+
return { kind: "ok" };
|
|
2485
|
+
}
|
|
2486
|
+
ctx.attempts.push({
|
|
2487
|
+
source: "github-meta",
|
|
2488
|
+
url: result.repoUrl,
|
|
2489
|
+
status: "not-found",
|
|
2490
|
+
message: "No homepage in repo metadata"
|
|
2491
|
+
});
|
|
2492
|
+
return { kind: "skip" };
|
|
2493
|
+
}
|
|
2494
|
+
});
|
|
2495
|
+
const githubReadmeResolver = defineResolver({
|
|
2496
|
+
id: "readme",
|
|
2497
|
+
canResolve: (ctx) => !!ctx.result?.repoUrl?.includes("github.com"),
|
|
2498
|
+
async run(ctx) {
|
|
2499
|
+
const result = ctx.result;
|
|
2500
|
+
const gh = parseGitHubUrl(result.repoUrl);
|
|
2501
|
+
if (!gh) return { kind: "skip" };
|
|
2502
|
+
ctx.options.onProgress?.("readme");
|
|
2503
|
+
const readmeUrl = await fetchReadme(gh.owner, gh.repo, ctx.subdir, result.gitRef);
|
|
2504
|
+
if (readmeUrl) {
|
|
2505
|
+
result.readmeUrl = readmeUrl;
|
|
2506
|
+
ctx.attempts.push({
|
|
2507
|
+
source: "readme",
|
|
2508
|
+
url: readmeUrl,
|
|
2509
|
+
status: "success"
|
|
2510
|
+
});
|
|
2511
|
+
return { kind: "ok" };
|
|
2512
|
+
}
|
|
2513
|
+
ctx.attempts.push({
|
|
2514
|
+
source: "readme",
|
|
2515
|
+
url: `${result.repoUrl}/README.md`,
|
|
2516
|
+
status: "not-found",
|
|
2517
|
+
message: "No README found"
|
|
2518
|
+
});
|
|
2519
|
+
return { kind: "skip" };
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
const githubSearchResolver = defineResolver({
|
|
2523
|
+
id: "github-search",
|
|
2524
|
+
canResolve: (ctx) => !!ctx.result && !ctx.result.repoUrl,
|
|
2525
|
+
async run(ctx) {
|
|
2526
|
+
const result = ctx.result;
|
|
2527
|
+
ctx.options.onProgress?.("github-search");
|
|
2528
|
+
const searchedUrl = await searchGitHubRepo(ctx.packageName);
|
|
2529
|
+
if (searchedUrl) {
|
|
2530
|
+
result.repoUrl = searchedUrl;
|
|
2531
|
+
ctx.attempts.push({
|
|
2532
|
+
source: "github-search",
|
|
2533
|
+
url: searchedUrl,
|
|
2534
|
+
status: "success",
|
|
2535
|
+
message: `Found via GitHub search: ${searchedUrl}`
|
|
2536
|
+
});
|
|
2537
|
+
return { kind: "ok" };
|
|
2538
|
+
}
|
|
2539
|
+
ctx.attempts.push({
|
|
2540
|
+
source: "github-search",
|
|
2541
|
+
status: "not-found",
|
|
2542
|
+
message: "No repository URL in package.json and GitHub search found no match"
|
|
2543
|
+
});
|
|
2544
|
+
return { kind: "skip" };
|
|
2545
|
+
}
|
|
2546
|
+
});
|
|
2547
|
+
const llmsTxtResolver = defineResolver({
|
|
2548
|
+
id: "llms.txt",
|
|
2549
|
+
canResolve: (ctx) => !!ctx.result?.docsUrl,
|
|
2550
|
+
async run(ctx) {
|
|
2551
|
+
const result = ctx.result;
|
|
2552
|
+
ctx.options.onProgress?.("llms.txt");
|
|
2553
|
+
const llmsUrl = await fetchLlmsUrl(result.docsUrl);
|
|
2554
|
+
if (llmsUrl) {
|
|
2555
|
+
result.llmsUrl = llmsUrl;
|
|
2556
|
+
ctx.attempts.push({
|
|
2557
|
+
source: "llms.txt",
|
|
2558
|
+
url: llmsUrl,
|
|
2559
|
+
status: "success"
|
|
2560
|
+
});
|
|
2561
|
+
} else ctx.attempts.push({
|
|
2562
|
+
source: "llms.txt",
|
|
2563
|
+
url: `${new URL(result.docsUrl).origin}/llms.txt`,
|
|
2564
|
+
status: "not-found",
|
|
2565
|
+
message: "No llms.txt at docs URL"
|
|
2566
|
+
});
|
|
2567
|
+
if (result.gitDocsUrl && result.llmsUrl && ctx.gitDocsAllFiles) {
|
|
2568
|
+
const llmsContent = await fetchLlmsTxt(result.llmsUrl);
|
|
2569
|
+
if (llmsContent && llmsContent.links.length > 0) {
|
|
2570
|
+
const validation = validateGitDocsWithLlms(llmsContent.links, ctx.gitDocsAllFiles);
|
|
2571
|
+
if (!validation.isValid) {
|
|
2572
|
+
ctx.attempts.push({
|
|
2573
|
+
source: "github-docs",
|
|
2574
|
+
url: result.gitDocsUrl,
|
|
2575
|
+
status: "not-found",
|
|
2576
|
+
message: `Heuristic git docs don't match llms.txt links (${Math.round(validation.matchRatio * 100)}% match), preferring llms.txt`
|
|
2577
|
+
});
|
|
2578
|
+
result.gitDocsUrl = void 0;
|
|
2579
|
+
result.gitRef = void 0;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return { kind: "ok" };
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
const localReadmeResolver = defineResolver({
|
|
2587
|
+
id: "local",
|
|
2588
|
+
canResolve: (ctx) => {
|
|
2589
|
+
const r = ctx.result;
|
|
2590
|
+
return !!r && !!ctx.options.cwd && !r.docsUrl && !r.llmsUrl && !r.readmeUrl && !r.gitDocsUrl;
|
|
2591
|
+
},
|
|
2592
|
+
async run(ctx) {
|
|
2593
|
+
const result = ctx.result;
|
|
2594
|
+
ctx.options.onProgress?.("local");
|
|
2595
|
+
const pkgDir = join(ctx.options.cwd, "node_modules", ctx.packageName);
|
|
2596
|
+
const readmeFile = existsSync(pkgDir) && readdirSync(pkgDir).find((f) => README_FILENAME_RE.test(f));
|
|
2597
|
+
if (readmeFile) {
|
|
2598
|
+
const readmePath = join(pkgDir, readmeFile);
|
|
2599
|
+
result.readmeUrl = pathToFileURL(readmePath).href;
|
|
2600
|
+
ctx.attempts.push({
|
|
2601
|
+
source: "readme",
|
|
2602
|
+
url: readmePath,
|
|
2603
|
+
status: "success",
|
|
2604
|
+
message: "Found local readme in node_modules"
|
|
2605
|
+
});
|
|
2606
|
+
return { kind: "ok" };
|
|
2607
|
+
}
|
|
2608
|
+
return { kind: "skip" };
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
const STATIC_REGEX_1 = /^github:/;
|
|
2612
|
+
const defaultResolvers = [
|
|
2613
|
+
defineResolver({
|
|
2614
|
+
id: "npm",
|
|
2615
|
+
async run(ctx) {
|
|
2616
|
+
ctx.options.onProgress?.("npm");
|
|
2617
|
+
const pkg = await fetchNpmPackage(ctx.packageName);
|
|
2618
|
+
if (!pkg) {
|
|
2619
|
+
ctx.attempts.push({
|
|
2620
|
+
source: "npm",
|
|
2621
|
+
url: `https://registry.npmjs.org/${ctx.packageName}/latest`,
|
|
2622
|
+
status: "not-found",
|
|
2623
|
+
message: "Package not found on npm registry"
|
|
2624
|
+
});
|
|
2625
|
+
return { kind: "fatal" };
|
|
2626
|
+
}
|
|
2627
|
+
ctx.attempts.push({
|
|
2628
|
+
source: "npm",
|
|
2629
|
+
url: `https://registry.npmjs.org/${ctx.packageName}/latest`,
|
|
2630
|
+
status: "success",
|
|
2631
|
+
message: `Found ${pkg.name}@${pkg.version}`
|
|
2632
|
+
});
|
|
2633
|
+
const registryMeta = pkg.version ? await fetchNpmRegistryMeta(ctx.packageName, pkg.version) : {};
|
|
2634
|
+
const result = {
|
|
2635
|
+
name: pkg.name,
|
|
2636
|
+
version: pkg.version,
|
|
2637
|
+
releasedAt: registryMeta.releasedAt,
|
|
2638
|
+
description: pkg.description,
|
|
2639
|
+
dependencies: pkg.dependencies,
|
|
2640
|
+
distTags: registryMeta.distTags
|
|
2641
|
+
};
|
|
2642
|
+
if (typeof pkg.repository === "object" && pkg.repository?.url) {
|
|
2643
|
+
ctx.rawRepoUrl = pkg.repository.url;
|
|
2644
|
+
const normalized = normalizeRepoUrl(pkg.repository.url);
|
|
2645
|
+
if (!normalized.includes("://") && normalized.includes("/") && !normalized.includes(":")) result.repoUrl = `https://github.com/${normalized}`;
|
|
2646
|
+
else result.repoUrl = normalized;
|
|
2647
|
+
ctx.subdir = pkg.repository.directory;
|
|
2648
|
+
} else if (typeof pkg.repository === "string") if (pkg.repository.includes("://")) {
|
|
2649
|
+
const gh = parseGitHubUrl(pkg.repository);
|
|
2650
|
+
if (gh) result.repoUrl = `https://github.com/${gh.owner}/${gh.repo}`;
|
|
2651
|
+
} else {
|
|
2652
|
+
const repo = pkg.repository.replace(STATIC_REGEX_1, "");
|
|
2653
|
+
if (repo.includes("/") && !repo.includes(":")) result.repoUrl = `https://github.com/${repo}`;
|
|
2654
|
+
}
|
|
2655
|
+
if (pkg.homepage && !isGitHubRepoUrl(pkg.homepage) && !isUselessDocsUrl(pkg.homepage)) result.docsUrl = pkg.homepage;
|
|
2656
|
+
ctx.npm = pkg;
|
|
2657
|
+
ctx.result = result;
|
|
2658
|
+
return { kind: "ok" };
|
|
2659
|
+
}
|
|
2660
|
+
}),
|
|
2661
|
+
githubSearchResolver,
|
|
2662
|
+
gitTagResolver,
|
|
2663
|
+
githubMetaResolver,
|
|
2664
|
+
githubReadmeResolver,
|
|
2665
|
+
crawlUrlResolver,
|
|
2666
|
+
llmsTxtResolver,
|
|
2667
|
+
localReadmeResolver
|
|
2668
|
+
];
|
|
2669
|
+
function defineResolver(r) {
|
|
2670
|
+
return r;
|
|
2671
|
+
}
|
|
2672
|
+
function createContentResolver(opts) {
|
|
2673
|
+
return { async resolve(packageName, options = {}) {
|
|
2674
|
+
const ctx = {
|
|
2675
|
+
packageName,
|
|
2676
|
+
options,
|
|
2677
|
+
result: null,
|
|
2678
|
+
attempts: []
|
|
2679
|
+
};
|
|
2680
|
+
let registryVersion;
|
|
2681
|
+
for (const resolver of opts.resolvers) {
|
|
2682
|
+
if (resolver.canResolve && !resolver.canResolve(ctx)) continue;
|
|
2683
|
+
const outcome = await resolver.run(ctx);
|
|
2684
|
+
if (outcome.kind === "fatal") return {
|
|
2685
|
+
package: null,
|
|
2686
|
+
attempts: ctx.attempts,
|
|
2687
|
+
registryVersion: outcome.registryVersion ?? registryVersion
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
registryVersion = ctx.npm?.version;
|
|
2691
|
+
const r = ctx.result;
|
|
2692
|
+
if (!r || !r.docsUrl && !r.llmsUrl && !r.readmeUrl && !r.gitDocsUrl) return {
|
|
2693
|
+
package: null,
|
|
2694
|
+
attempts: ctx.attempts,
|
|
2695
|
+
registryVersion
|
|
2696
|
+
};
|
|
2697
|
+
return {
|
|
2698
|
+
package: r,
|
|
2699
|
+
attempts: ctx.attempts,
|
|
2700
|
+
registryVersion
|
|
2701
|
+
};
|
|
2702
|
+
} };
|
|
2703
|
+
}
|
|
2704
|
+
const defaultContentResolver = createContentResolver({ resolvers: defaultResolvers });
|
|
2705
|
+
async function resolvePackageDocs(packageName, options = {}) {
|
|
2706
|
+
return (await defaultContentResolver.resolve(packageName, options)).package;
|
|
2707
|
+
}
|
|
2708
|
+
async function resolvePackageDocsWithAttempts(packageName, options = {}) {
|
|
2709
|
+
return defaultContentResolver.resolve(packageName, options);
|
|
2710
|
+
}
|
|
2711
|
+
const RESOLVE_STEP_LABELS = {
|
|
2712
|
+
"npm": "npm registry",
|
|
2713
|
+
"github-docs": "GitHub docs",
|
|
2714
|
+
"github-meta": "GitHub meta",
|
|
2715
|
+
"github-search": "GitHub search",
|
|
2716
|
+
"readme": "README",
|
|
2717
|
+
"llms.txt": "llms.txt",
|
|
2718
|
+
"crawl": "website crawl",
|
|
2719
|
+
"local": "node_modules"
|
|
2720
|
+
};
|
|
2721
|
+
async function resolvePackageOrCrate(packageSpec, opts) {
|
|
2722
|
+
const { cwd, onProgress } = opts;
|
|
2723
|
+
const isCrate = isCrateSpec(packageSpec);
|
|
2724
|
+
const normalizedSpec = isCrate ? packageSpec.slice(6).trim() : packageSpec;
|
|
2725
|
+
const { name: parsedName, tag: requestedTag } = parsePackageSpec(normalizedSpec);
|
|
2726
|
+
const packageName = isCrate ? parsedName.toLowerCase() : parsedName;
|
|
2727
|
+
const identityPackageName = isCrate ? toCrateIdentity(packageName) : packageName;
|
|
2728
|
+
const storagePackageName = toStoragePackageName(identityPackageName);
|
|
2729
|
+
const localDeps = isCrate ? [] : await readLocalDependencies(cwd).catch(() => []);
|
|
2730
|
+
const localVersion = isCrate ? void 0 : localDeps.find((d) => d.name === packageName)?.version;
|
|
2731
|
+
const resolveResult = isCrate ? await resolveCrateDocsWithAttempts(packageName, {
|
|
2732
|
+
version: requestedTag,
|
|
2733
|
+
onProgress
|
|
2734
|
+
}) : await resolvePackageDocsWithAttempts(requestedTag ? normalizedSpec : packageName, {
|
|
2735
|
+
version: localVersion,
|
|
2736
|
+
cwd,
|
|
2737
|
+
onProgress: (step) => onProgress?.(RESOLVE_STEP_LABELS[step] ?? step)
|
|
2738
|
+
});
|
|
2739
|
+
let resolved = resolveResult.package;
|
|
2740
|
+
if (!resolved && !isCrate) {
|
|
2741
|
+
onProgress?.(RESOLVE_STEP_LABELS.local);
|
|
2742
|
+
resolved = await resolveLocalDep(packageName, cwd);
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
packageName,
|
|
2746
|
+
identityPackageName,
|
|
2747
|
+
storagePackageName,
|
|
2748
|
+
isCrate,
|
|
2749
|
+
requestedTag,
|
|
2750
|
+
localVersion,
|
|
2751
|
+
resolved,
|
|
2752
|
+
attempts: resolveResult.attempts,
|
|
2753
|
+
registryVersion: resolveResult.registryVersion
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
function semverValid(v) {
|
|
2757
|
+
return valid(v, true);
|
|
2758
|
+
}
|
|
2759
|
+
function semverGt(a, b) {
|
|
2760
|
+
return gt(a, b, true);
|
|
2761
|
+
}
|
|
2762
|
+
function semverDiff(a, b) {
|
|
2763
|
+
return diff(a, b);
|
|
2474
2764
|
}
|
|
2475
|
-
export {
|
|
2765
|
+
export { fetchGitHubIssues as A, fetchBlogReleases as B, fetchCrawledDocs as C, downloadLlmsDocs as D, resolveGitHubRepo as E, filterFrameworkDocs as F, generateReleaseIndex as H, isShallowGitDocs as I, parseGitHubRepoSlug as L, generateIssueIndex as M, isGhAvailable as N, fetchLlmsTxt as O, fetchGitDocs as P, parseGitHubUrl as R, generateDiscussionIndex as S, fetchReadmeContent as T, isPrerelease as U, fetchReleaseNotes as V, fetchGitHubRaw as W, fetchGitSkills as _, resolvePackageDocs as a, fetchGitHubDiscussions as b, resolveSkillName as c, fetchNpmPackage as d, fetchNpmRegistryMeta as f, readLocalPackageInfo as g, readLocalDependencies as h, resolvePackageOrCrate as i, formatIssueAsMarkdown as j, normalizeLlmsLinks as k, toStoragePackageName as l, searchNpmPackages as m, semverGt as n, isCrateSpec as o, fetchPkgDist as p, semverValid as r, parseSkillInput as s, semverDiff as t, fetchLatestVersion as u, resolveEntryFiles as v, toCrawlPattern as w, formatDiscussionAsMarkdown as x, generateDocsIndex as y, parsePackageSpec as z };
|
|
2476
2766
|
|
|
2477
|
-
//# sourceMappingURL=
|
|
2767
|
+
//# sourceMappingURL=semver.mjs.map
|