skilld 1.7.4 → 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 +81 -57
- 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 +10 -7
- package/dist/_chunks/assemble.mjs.map +1 -1
- package/dist/_chunks/author.mjs +33 -17
- package/dist/_chunks/author.mjs.map +1 -1
- package/dist/_chunks/cache.mjs +143 -183
- 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 +5 -5
- package/dist/_chunks/detect.mjs +53 -43
- 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 +1 -1
- 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 +81 -326
- 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 +3 -2
- 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 +4 -2
- package/dist/_chunks/monorepo.mjs.map +1 -1
- package/dist/_chunks/package-json.mjs.map +1 -1
- package/dist/_chunks/paths.mjs +3 -5
- package/dist/_chunks/paths.mjs.map +1 -1
- package/dist/_chunks/{sync-pipeline.mjs → pipeline.mjs} +346 -313
- package/dist/_chunks/pipeline.mjs.map +1 -0
- package/dist/_chunks/pool2.mjs +1 -1
- 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 +7 -6
- package/dist/_chunks/prepare2.mjs.map +1 -1
- package/dist/_chunks/prompts.mjs +484 -74
- 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 +8 -6
- 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 -3
- package/dist/_chunks/search.mjs.map +1 -1
- package/dist/_chunks/semver.mjs +2755 -1
- package/dist/_chunks/semver.mjs.map +1 -1
- package/dist/_chunks/skill-installer2.mjs +10 -11
- package/dist/_chunks/skill-installer2.mjs.map +1 -1
- package/dist/_chunks/skills.mjs +6 -7
- 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 +411 -910
- package/dist/_chunks/sync.mjs.map +1 -1
- package/dist/_chunks/sync2.mjs +2 -5
- package/dist/_chunks/telemetry.mjs +26 -0
- package/dist/_chunks/telemetry.mjs.map +1 -0
- package/dist/_chunks/uninstall.mjs +12 -9
- 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 +3 -3
- package/dist/_chunks/validate.mjs +1 -1
- package/dist/_chunks/version.mjs +16 -17
- 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 +72 -53
- package/dist/cli.mjs.map +1 -1
- package/dist/prepare.mjs +4 -3
- 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 +19 -28
- 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/index.d.mts +0 -344
- package/dist/_chunks/index.d.mts.map +0 -1
- package/dist/_chunks/index2.d.mts +0 -279
- package/dist/_chunks/index2.d.mts.map +0 -1
- package/dist/_chunks/index3.d.mts +0 -44
- package/dist/_chunks/index3.d.mts.map +0 -1
- package/dist/_chunks/index4.d.mts +0 -553
- package/dist/_chunks/index4.d.mts.map +0 -1
- package/dist/_chunks/package-registry.mjs +0 -465
- package/dist/_chunks/package-registry.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/sources.mjs +0 -2654
- package/dist/_chunks/sources.mjs.map +0 -1
- package/dist/_chunks/sync-pipeline.mjs.map +0 -1
- package/dist/_chunks/sync-registry.mjs +0 -65
- package/dist/_chunks/sync-registry.mjs.map +0 -1
- package/dist/_chunks/types.d.mts +0 -76
- package/dist/_chunks/types.d.mts.map +0 -1
- package/dist/_chunks/types2.d.mts +0 -88
- package/dist/_chunks/types2.d.mts.map +0 -1
- package/dist/_chunks/wizard.mjs.map +0 -1
- package/dist/agent/index.d.mts +0 -2
- package/dist/agent/index.mjs +0 -4
- package/dist/cache/index.d.mts +0 -2
- package/dist/cache/index.mjs +0 -5
- package/dist/index.d.mts +0 -6
- package/dist/index.mjs +0 -6
- package/dist/retriv/index.d.mts +0 -3
- package/dist/retriv/index.mjs +0 -2
- package/dist/sources/index.d.mts +0 -3
- package/dist/sources/index.mjs +0 -3
- package/dist/types.d.mts +0 -4
- package/dist/types.mjs +0 -1
package/dist/_chunks/semver.mjs
CHANGED
|
@@ -1,4 +1,2758 @@
|
|
|
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
|
+
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";
|
|
8
|
+
import { i as parseFrontmatter, n as extractLinks, r as extractTitle, t as extractDescription } from "./markdown.mjs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
11
|
+
import pLimit from "p-limit";
|
|
12
|
+
import { basename, dirname, join, resolve } from "pathe";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { glob } from "node:fs/promises";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
import { htmlToMarkdown } from "mdream";
|
|
18
|
+
import { ofetch } from "ofetch";
|
|
19
|
+
import { crawlAndGenerate } from "@mdream/crawl";
|
|
20
|
+
import { downloadTemplate } from "giget";
|
|
21
|
+
import { Writable } from "node:stream";
|
|
1
22
|
import { diff, gt, valid } from "semver";
|
|
23
|
+
const STATIC_REGEX_1$9 = /```[\s\S]*?```/;
|
|
24
|
+
const STATIC_REGEX_2$4 = /`[^`]+`/;
|
|
25
|
+
const BOT_USERS = new Set([
|
|
26
|
+
"renovate[bot]",
|
|
27
|
+
"dependabot[bot]",
|
|
28
|
+
"renovate-bot",
|
|
29
|
+
"dependabot",
|
|
30
|
+
"github-actions[bot]"
|
|
31
|
+
]);
|
|
32
|
+
const isoDate = (iso) => iso.split("T")[0];
|
|
33
|
+
function buildFrontmatter(fields) {
|
|
34
|
+
const lines = ["---"];
|
|
35
|
+
for (const [k, v] of Object.entries(fields)) if (v !== void 0) lines.push(`${k}: ${typeof v === "string" ? yamlEscape(v) : v}`);
|
|
36
|
+
lines.push("---");
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
function hasCodeBlock(text) {
|
|
40
|
+
return STATIC_REGEX_1$9.test(text) || STATIC_REGEX_2$4.test(text);
|
|
41
|
+
}
|
|
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;
|
|
43
|
+
function truncateBody(body, limit) {
|
|
44
|
+
if (body.length <= limit) return body;
|
|
45
|
+
const codeBlockRe = /```[\s\S]*?```/g;
|
|
46
|
+
let lastSafeEnd = limit;
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = codeBlockRe.exec(body)) !== null) {
|
|
49
|
+
const blockStart = match.index;
|
|
50
|
+
const blockEnd = blockStart + match[0].length;
|
|
51
|
+
if (blockStart < limit && blockEnd > limit) {
|
|
52
|
+
if (blockEnd <= limit + 500) lastSafeEnd = blockEnd;
|
|
53
|
+
else lastSafeEnd = blockStart;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const slice = body.slice(0, lastSafeEnd);
|
|
58
|
+
const lastParagraph = slice.lastIndexOf("\n\n");
|
|
59
|
+
if (lastParagraph > lastSafeEnd * .6) return `${slice.slice(0, lastParagraph)}\n\n...`;
|
|
60
|
+
return `${slice}...`;
|
|
61
|
+
}
|
|
62
|
+
let _ghToken;
|
|
63
|
+
function getGitHubToken() {
|
|
64
|
+
if (_ghToken !== void 0) return _ghToken;
|
|
65
|
+
try {
|
|
66
|
+
const { stdout } = spawnSync("gh", ["auth", "token"], {
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
timeout: 5e3,
|
|
69
|
+
stdio: [
|
|
70
|
+
"ignore",
|
|
71
|
+
"pipe",
|
|
72
|
+
"ignore"
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
_ghToken = stdout?.trim() || null;
|
|
76
|
+
} catch {
|
|
77
|
+
_ghToken = null;
|
|
78
|
+
}
|
|
79
|
+
return _ghToken;
|
|
80
|
+
}
|
|
81
|
+
const _needsAuth = /* @__PURE__ */ new Set();
|
|
82
|
+
function markRepoPrivate(owner, repo) {
|
|
83
|
+
_needsAuth.add(`${owner}/${repo}`);
|
|
84
|
+
}
|
|
85
|
+
function isKnownPrivateRepo(owner, repo) {
|
|
86
|
+
return _needsAuth.has(`${owner}/${repo}`);
|
|
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
|
+
}
|
|
97
|
+
const GH_API = "https://api.github.com";
|
|
98
|
+
const ghApiFetch = ofetch.create({
|
|
99
|
+
retry: 2,
|
|
100
|
+
retryDelay: 500,
|
|
101
|
+
timeout: 15e3,
|
|
102
|
+
headers: { "User-Agent": "skilld/1.0" }
|
|
103
|
+
});
|
|
104
|
+
const LINK_NEXT_RE = /<([^>]+)>;\s*rel="next"/;
|
|
105
|
+
function parseLinkNext(header) {
|
|
106
|
+
if (!header) return null;
|
|
107
|
+
return header.match(LINK_NEXT_RE)?.[1] ?? null;
|
|
108
|
+
}
|
|
109
|
+
async function ghApi(endpoint) {
|
|
110
|
+
const token = getGitHubToken();
|
|
111
|
+
if (!token) return null;
|
|
112
|
+
return ghApiFetch(`${GH_API}/${endpoint}`, { headers: { Authorization: `token ${token}` } }).catch(() => null);
|
|
113
|
+
}
|
|
114
|
+
async function ghApiPaginated(endpoint) {
|
|
115
|
+
const token = getGitHubToken();
|
|
116
|
+
if (!token) return [];
|
|
117
|
+
const headers = { Authorization: `token ${token}` };
|
|
118
|
+
const results = [];
|
|
119
|
+
let url = `${GH_API}/${endpoint}`;
|
|
120
|
+
while (url) {
|
|
121
|
+
const res = await ghApiFetch.raw(url, { headers }).catch(() => null);
|
|
122
|
+
if (!res?.ok || !Array.isArray(res._data)) break;
|
|
123
|
+
results.push(...res._data);
|
|
124
|
+
url = parseLinkNext(res.headers.get("link"));
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
const SKILLD_USER_AGENT = "skilld/1.0 (+https://github.com/harlan-zw/skilld)";
|
|
129
|
+
const $fetch = ofetch.create({
|
|
130
|
+
retry: 3,
|
|
131
|
+
retryDelay: 1e3,
|
|
132
|
+
retryStatusCodes: [
|
|
133
|
+
408,
|
|
134
|
+
429,
|
|
135
|
+
500,
|
|
136
|
+
502,
|
|
137
|
+
503,
|
|
138
|
+
504
|
|
139
|
+
],
|
|
140
|
+
timeout: 15e3,
|
|
141
|
+
headers: { "User-Agent": SKILLD_USER_AGENT }
|
|
142
|
+
});
|
|
143
|
+
function createRateLimitedRunner(intervalMs) {
|
|
144
|
+
let queue = Promise.resolve();
|
|
145
|
+
let lastRunAt = 0;
|
|
146
|
+
return async function runRateLimited(task) {
|
|
147
|
+
const run = async () => {
|
|
148
|
+
const waitMs = intervalMs - (Date.now() - lastRunAt);
|
|
149
|
+
if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
150
|
+
lastRunAt = Date.now();
|
|
151
|
+
return task();
|
|
152
|
+
};
|
|
153
|
+
const request = queue.then(run, run);
|
|
154
|
+
queue = request.then(() => void 0, () => void 0);
|
|
155
|
+
return request;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function fetchText(url) {
|
|
159
|
+
return $fetch(url, { responseType: "text" }).catch(() => null);
|
|
160
|
+
}
|
|
161
|
+
const RAW_GH_RE = /raw\.githubusercontent\.com\/([^/]+)\/([^/]+)/;
|
|
162
|
+
function extractGitHubRepo(url) {
|
|
163
|
+
const match = url.match(RAW_GH_RE);
|
|
164
|
+
return match ? {
|
|
165
|
+
owner: match[1],
|
|
166
|
+
repo: match[2]
|
|
167
|
+
} : null;
|
|
168
|
+
}
|
|
169
|
+
async function fetchGitHubRaw(url) {
|
|
170
|
+
const gh = extractGitHubRepo(url);
|
|
171
|
+
if (!(gh ? isKnownPrivateRepo(gh.owner, gh.repo) : false)) {
|
|
172
|
+
const content = await fetchText(url);
|
|
173
|
+
if (content) return content;
|
|
174
|
+
}
|
|
175
|
+
if (!gh) return null;
|
|
176
|
+
const token = getGitHubToken();
|
|
177
|
+
if (!token) return null;
|
|
178
|
+
const content = await $fetch(url, {
|
|
179
|
+
responseType: "text",
|
|
180
|
+
headers: { Authorization: `token ${token}` }
|
|
181
|
+
}).catch(() => null);
|
|
182
|
+
if (content) markRepoPrivate(gh.owner, gh.repo);
|
|
183
|
+
return content;
|
|
184
|
+
}
|
|
185
|
+
async function verifyUrl(url) {
|
|
186
|
+
const res = await $fetch.raw(url, { method: "HEAD" }).catch(() => null);
|
|
187
|
+
if (!res) return false;
|
|
188
|
+
return !(res.headers.get("content-type") || "").includes("text/html");
|
|
189
|
+
}
|
|
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;
|
|
193
|
+
function parseSemver(version) {
|
|
194
|
+
const clean = version.replace(V_PREFIX_RE, "");
|
|
195
|
+
const match = clean.match(STATIC_REGEX_2$3);
|
|
196
|
+
if (!match) return null;
|
|
197
|
+
return {
|
|
198
|
+
major: +match[1],
|
|
199
|
+
minor: match[2] ? +match[2] : 0,
|
|
200
|
+
patch: match[3] ? +match[3] : 0,
|
|
201
|
+
raw: clean
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function extractVersion(tag, packageName) {
|
|
205
|
+
if (packageName) {
|
|
206
|
+
const atMatch = tag.match(new RegExp(`^${escapeRegex(packageName)}@(.+)$`));
|
|
207
|
+
if (atMatch) return atMatch[1];
|
|
208
|
+
const dashMatch = tag.match(new RegExp(`^${escapeRegex(packageName)}-v?(.+)$`));
|
|
209
|
+
if (dashMatch) return dashMatch[1];
|
|
210
|
+
}
|
|
211
|
+
return tag.replace(V_PREFIX_RE, "");
|
|
212
|
+
}
|
|
213
|
+
function escapeRegex(str) {
|
|
214
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
215
|
+
}
|
|
216
|
+
function tagMatchesPackage(tag, packageName) {
|
|
217
|
+
return tag.startsWith(`${packageName}@`) || tag.startsWith(`${packageName}-v`) || tag.startsWith(`${packageName}-`);
|
|
218
|
+
}
|
|
219
|
+
function isPrerelease(version) {
|
|
220
|
+
return STATIC_REGEX_3$3.test(version.replace(V_PREFIX_RE, ""));
|
|
221
|
+
}
|
|
222
|
+
function compareSemver(a, b) {
|
|
223
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
224
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
225
|
+
return a.patch - b.patch;
|
|
226
|
+
}
|
|
227
|
+
function mapApiRelease(r) {
|
|
228
|
+
return {
|
|
229
|
+
id: r.id,
|
|
230
|
+
tag: r.tag_name,
|
|
231
|
+
name: r.name,
|
|
232
|
+
prerelease: r.prerelease,
|
|
233
|
+
createdAt: r.created_at,
|
|
234
|
+
publishedAt: r.published_at,
|
|
235
|
+
markdown: r.body
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async function fetchAllReleases(owner, repo) {
|
|
239
|
+
const apiReleases = await ghApiPaginated(`repos/${owner}/${repo}/releases`);
|
|
240
|
+
if (apiReleases.length > 0) return apiReleases.map(mapApiRelease);
|
|
241
|
+
return (await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`, { signal: AbortSignal.timeout(15e3) }).catch(() => null))?.releases ?? [];
|
|
242
|
+
}
|
|
243
|
+
function selectReleases(releases, packageName, installedVersion, fromDate) {
|
|
244
|
+
const hasMonorepoTags = packageName && releases.some((r) => tagMatchesPackage(r.tag, packageName));
|
|
245
|
+
const installedSv = installedVersion ? parseSemver(installedVersion) : null;
|
|
246
|
+
const installedIsPrerelease = installedVersion ? isPrerelease(installedVersion) : false;
|
|
247
|
+
const fromTs = fromDate ? new Date(fromDate).getTime() : null;
|
|
248
|
+
const sorted = releases.filter((r) => {
|
|
249
|
+
const ver = extractVersion(r.tag, hasMonorepoTags ? packageName : void 0);
|
|
250
|
+
if (!ver) return false;
|
|
251
|
+
const sv = parseSemver(ver);
|
|
252
|
+
if (!sv) return false;
|
|
253
|
+
if (hasMonorepoTags && packageName && !tagMatchesPackage(r.tag, packageName)) return false;
|
|
254
|
+
if (fromTs) {
|
|
255
|
+
const pubDate = r.publishedAt || r.createdAt;
|
|
256
|
+
if (pubDate && new Date(pubDate).getTime() < fromTs) return false;
|
|
257
|
+
}
|
|
258
|
+
if (r.prerelease) {
|
|
259
|
+
if (!installedIsPrerelease || !installedSv) return false;
|
|
260
|
+
return sv.major === installedSv.major && sv.minor === installedSv.minor;
|
|
261
|
+
}
|
|
262
|
+
if (installedSv && compareSemver(sv, installedSv) > 0) return false;
|
|
263
|
+
return true;
|
|
264
|
+
}).sort((a, b) => {
|
|
265
|
+
const verA = extractVersion(a.tag, hasMonorepoTags ? packageName : void 0);
|
|
266
|
+
const verB = extractVersion(b.tag, hasMonorepoTags ? packageName : void 0);
|
|
267
|
+
if (!verA || !verB) return 0;
|
|
268
|
+
return compareSemver(parseSemver(verB), parseSemver(verA));
|
|
269
|
+
});
|
|
270
|
+
return fromDate ? sorted : sorted.slice(0, 20);
|
|
271
|
+
}
|
|
272
|
+
function formatRelease(release, packageName) {
|
|
273
|
+
const date = isoDate(release.publishedAt || release.createdAt);
|
|
274
|
+
const version = extractVersion(release.tag, packageName) || release.tag;
|
|
275
|
+
const fm = [
|
|
276
|
+
"---",
|
|
277
|
+
`tag: ${yamlEscape(release.tag)}`,
|
|
278
|
+
`version: ${yamlEscape(version)}`,
|
|
279
|
+
`published: ${date}`
|
|
280
|
+
];
|
|
281
|
+
if (release.name && release.name !== release.tag) fm.push(`name: ${yamlEscape(release.name)}`);
|
|
282
|
+
fm.push("---");
|
|
283
|
+
return `${fm.join("\n")}\n\n# ${release.name || release.tag}\n\n${release.markdown}`;
|
|
284
|
+
}
|
|
285
|
+
function generateReleaseIndex(releasesOrOpts, packageName) {
|
|
286
|
+
const opts = Array.isArray(releasesOrOpts) ? {
|
|
287
|
+
releases: releasesOrOpts,
|
|
288
|
+
packageName
|
|
289
|
+
} : releasesOrOpts;
|
|
290
|
+
const { releases, blogReleases, hasChangelog } = opts;
|
|
291
|
+
const pkg = opts.packageName;
|
|
292
|
+
const lines = [
|
|
293
|
+
[
|
|
294
|
+
"---",
|
|
295
|
+
`total: ${releases.length + (blogReleases?.length ?? 0)}`,
|
|
296
|
+
`latest: ${yamlEscape(releases[0]?.tag || "unknown")}`,
|
|
297
|
+
"---"
|
|
298
|
+
].join("\n"),
|
|
299
|
+
"",
|
|
300
|
+
"# Releases Index",
|
|
301
|
+
""
|
|
302
|
+
];
|
|
303
|
+
if (blogReleases && blogReleases.length > 0) {
|
|
304
|
+
lines.push("## Blog Releases", "");
|
|
305
|
+
for (const b of blogReleases) lines.push(`- [${b.version}](./blog-${b.version}.md): ${b.title} (${b.date})`);
|
|
306
|
+
lines.push("");
|
|
307
|
+
}
|
|
308
|
+
if (releases.length > 0) {
|
|
309
|
+
if (blogReleases && blogReleases.length > 0) lines.push("## Release Notes", "");
|
|
310
|
+
for (const r of releases) {
|
|
311
|
+
const date = isoDate(r.publishedAt || r.createdAt);
|
|
312
|
+
const filename = r.tag.includes("@") || r.tag.startsWith("v") ? r.tag : `v${r.tag}`;
|
|
313
|
+
const sv = parseSemver(extractVersion(r.tag, pkg) || r.tag);
|
|
314
|
+
const label = sv?.patch === 0 && sv.minor === 0 ? " **[MAJOR]**" : sv?.patch === 0 ? " **[MINOR]**" : "";
|
|
315
|
+
lines.push(`- [${r.tag}](./${filename}.md): ${r.name || r.tag} (${date})${label}`);
|
|
316
|
+
}
|
|
317
|
+
lines.push("");
|
|
318
|
+
}
|
|
319
|
+
if (hasChangelog) {
|
|
320
|
+
lines.push("## Changelog", "");
|
|
321
|
+
lines.push("- [CHANGELOG.md](./CHANGELOG.md)");
|
|
322
|
+
lines.push("");
|
|
323
|
+
}
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
function isStubRelease(release) {
|
|
327
|
+
const body = (release.markdown || "").trim();
|
|
328
|
+
return body.length < 500 && STATIC_REGEX_4$1.test(body);
|
|
329
|
+
}
|
|
330
|
+
async function fetchChangelog(owner, repo, ref, packageName) {
|
|
331
|
+
const paths = [];
|
|
332
|
+
if (packageName) {
|
|
333
|
+
const shortName = packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "");
|
|
334
|
+
const scopeless = packageName.replace(NPM_SCOPE_PREFIX_RE, "").replace("/", "-");
|
|
335
|
+
const candidates = [...new Set([shortName, scopeless])];
|
|
336
|
+
for (const name of candidates) paths.push(`packages/${name}/CHANGELOG.md`);
|
|
337
|
+
}
|
|
338
|
+
paths.push("CHANGELOG.md", "changelog.md", "CHANGES.md");
|
|
339
|
+
for (const path of paths) {
|
|
340
|
+
const content = await fetchGitHubRaw(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`);
|
|
341
|
+
if (content) return content;
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageName, fromDate, changelogRef) {
|
|
346
|
+
const selected = selectReleases(await fetchAllReleases(owner, repo), packageName, installedVersion, fromDate);
|
|
347
|
+
if (selected.length > 0) {
|
|
348
|
+
const docs = selected.filter((r) => !isStubRelease(r)).map((r) => {
|
|
349
|
+
return {
|
|
350
|
+
path: `releases/${r.tag.includes("@") || r.tag.startsWith("v") ? r.tag : `v${r.tag}`}.md`,
|
|
351
|
+
content: formatRelease(r, packageName)
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
const changelog = await fetchChangelog(owner, repo, changelogRef || gitRef || selected[0].tag, packageName);
|
|
355
|
+
if (changelog && changelog.length < 5e5) docs.push({
|
|
356
|
+
path: "releases/CHANGELOG.md",
|
|
357
|
+
content: changelog
|
|
358
|
+
});
|
|
359
|
+
return docs;
|
|
360
|
+
}
|
|
361
|
+
const changelog = await fetchChangelog(owner, repo, changelogRef || gitRef || "main", packageName);
|
|
362
|
+
if (!changelog) return [];
|
|
363
|
+
return [{
|
|
364
|
+
path: "releases/CHANGELOG.md",
|
|
365
|
+
content: changelog
|
|
366
|
+
}];
|
|
367
|
+
}
|
|
368
|
+
const STATIC_REGEX_1$8 = /<h1[^>]*>([^<]+)<\/h1>/;
|
|
369
|
+
const STATIC_REGEX_2$2 = /<title>([^<]+)<\/title>/;
|
|
370
|
+
function formatBlogRelease(release) {
|
|
371
|
+
return `${[
|
|
372
|
+
"---",
|
|
373
|
+
`version: ${yamlEscape(release.version)}`,
|
|
374
|
+
`title: ${yamlEscape(release.title)}`,
|
|
375
|
+
`date: ${release.date}`,
|
|
376
|
+
`url: ${yamlEscape(release.url)}`,
|
|
377
|
+
`source: blog-release`,
|
|
378
|
+
"---"
|
|
379
|
+
].join("\n")}\n\n# ${release.title}\n\n${release.markdown}`;
|
|
380
|
+
}
|
|
381
|
+
async function fetchBlogPost(entry) {
|
|
382
|
+
try {
|
|
383
|
+
const html = await $fetch(entry.url, {
|
|
384
|
+
responseType: "text",
|
|
385
|
+
signal: AbortSignal.timeout(1e4)
|
|
386
|
+
}).catch(() => null);
|
|
387
|
+
if (!html) return null;
|
|
388
|
+
let title = "";
|
|
389
|
+
const titleMatch = html.match(STATIC_REGEX_1$8);
|
|
390
|
+
if (titleMatch) title = titleMatch[1].trim();
|
|
391
|
+
if (!title) {
|
|
392
|
+
const metaTitleMatch = html.match(STATIC_REGEX_2$2);
|
|
393
|
+
if (metaTitleMatch) title = metaTitleMatch[1].trim();
|
|
394
|
+
}
|
|
395
|
+
const markdown = htmlToMarkdown(html);
|
|
396
|
+
if (!markdown) return null;
|
|
397
|
+
return {
|
|
398
|
+
version: entry.version,
|
|
399
|
+
title: title || entry.title || `Release ${entry.version}`,
|
|
400
|
+
date: entry.date,
|
|
401
|
+
markdown,
|
|
402
|
+
url: entry.url
|
|
403
|
+
};
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function filterBlogsByVersion(entries, installedVersion) {
|
|
409
|
+
const installedSv = parseSemver(installedVersion);
|
|
410
|
+
if (!installedSv) return entries;
|
|
411
|
+
return entries.filter((entry) => {
|
|
412
|
+
const entrySv = parseSemver(entry.version);
|
|
413
|
+
if (!entrySv) return false;
|
|
414
|
+
return compareSemver(entrySv, installedSv) <= 0;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async function fetchBlogReleases(packageName, installedVersion) {
|
|
418
|
+
const preset = getBlogPreset(packageName);
|
|
419
|
+
if (!preset) return [];
|
|
420
|
+
const filteredReleases = filterBlogsByVersion(preset.releases, installedVersion);
|
|
421
|
+
if (filteredReleases.length === 0) return [];
|
|
422
|
+
const limit = pLimit(3);
|
|
423
|
+
const releases = (await Promise.all(filteredReleases.map((entry) => limit(() => fetchBlogPost(entry))))).filter((r) => r !== null);
|
|
424
|
+
if (releases.length === 0) return [];
|
|
425
|
+
releases.sort((a, b) => {
|
|
426
|
+
const aVer = a.version.split(".").map(Number);
|
|
427
|
+
const bVer = b.version.split(".").map(Number);
|
|
428
|
+
for (let i = 0; i < Math.max(aVer.length, bVer.length); i++) {
|
|
429
|
+
const diff = (bVer[i] ?? 0) - (aVer[i] ?? 0);
|
|
430
|
+
if (diff !== 0) return diff;
|
|
431
|
+
}
|
|
432
|
+
return 0;
|
|
433
|
+
});
|
|
434
|
+
return releases.map((r) => ({
|
|
435
|
+
path: `releases/blog-${r.version}.md`,
|
|
436
|
+
content: formatBlogRelease(r)
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
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
|
+
};
|
|
451
|
+
}
|
|
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 };
|
|
468
|
+
}
|
|
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 };
|
|
475
|
+
}
|
|
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/");
|
|
478
|
+
}
|
|
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;
|
|
485
|
+
}
|
|
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
|
+
}
|
|
493
|
+
}
|
|
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
|
+
}
|
|
507
|
+
}
|
|
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
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function isSafeUrl(url) {
|
|
528
|
+
try {
|
|
529
|
+
const parsed = new URL(url);
|
|
530
|
+
if (parsed.protocol !== "https:") return false;
|
|
531
|
+
const host = parsed.hostname;
|
|
532
|
+
if (host === "localhost" || host === "0.0.0.0" || host === "[::1]") return false;
|
|
533
|
+
if (STATIC_REGEX_8.test(host)) return false;
|
|
534
|
+
if (STATIC_REGEX_9.test(host)) return false;
|
|
535
|
+
return true;
|
|
536
|
+
} catch {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
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
|
+
}) ?? [];
|
|
548
|
+
}
|
|
549
|
+
async function findGitTag(owner, repo, version, packageName, branchHint) {
|
|
550
|
+
const candidates = [`v${version}`, version];
|
|
551
|
+
if (packageName) candidates.push(`${packageName}@${version}`);
|
|
552
|
+
for (const tag of candidates) {
|
|
553
|
+
const files = await listFilesAtRef(owner, repo, tag);
|
|
554
|
+
if (files.length > 0) return {
|
|
555
|
+
ref: tag,
|
|
556
|
+
files
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (packageName) {
|
|
560
|
+
const latestTag = await findLatestReleaseTag(owner, repo, packageName);
|
|
561
|
+
if (latestTag) {
|
|
562
|
+
const files = await listFilesAtRef(owner, repo, latestTag);
|
|
563
|
+
if (files.length > 0) return {
|
|
564
|
+
ref: latestTag,
|
|
565
|
+
files
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const branches = branchHint ? [branchHint, ...["main", "master"].filter((b) => b !== branchHint)] : ["main", "master"];
|
|
570
|
+
for (const branch of branches) {
|
|
571
|
+
const files = await listFilesAtRef(owner, repo, branch);
|
|
572
|
+
if (files.length > 0) return {
|
|
573
|
+
ref: branch,
|
|
574
|
+
files,
|
|
575
|
+
fallback: true
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
async function fetchUnghReleases(owner, repo) {
|
|
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) => ({
|
|
587
|
+
tag: r.tag_name,
|
|
588
|
+
publishedAt: r.published_at
|
|
589
|
+
})) : null;
|
|
590
|
+
}) ?? [];
|
|
591
|
+
}
|
|
592
|
+
async function findLatestReleaseTag(owner, repo, packageName) {
|
|
593
|
+
const prefix = `${packageName}@`;
|
|
594
|
+
return (await fetchUnghReleases(owner, repo)).find((r) => r.tag.startsWith(prefix))?.tag ?? null;
|
|
595
|
+
}
|
|
596
|
+
const STATIC_REGEX_1$6 = /\.(?:md|mdx)$/;
|
|
597
|
+
const isShallowGitDocs = (n) => n > 0 && n < 5;
|
|
598
|
+
function filterDocFiles(files, pathPrefix) {
|
|
599
|
+
return files.filter((f) => f.startsWith(pathPrefix) && STATIC_REGEX_1$6.test(f));
|
|
600
|
+
}
|
|
601
|
+
const FRAMEWORK_NAMES = new Set([
|
|
602
|
+
"vue",
|
|
603
|
+
"react",
|
|
604
|
+
"solid",
|
|
605
|
+
"angular",
|
|
606
|
+
"svelte",
|
|
607
|
+
"preact",
|
|
608
|
+
"lit",
|
|
609
|
+
"qwik"
|
|
610
|
+
]);
|
|
611
|
+
function filterFrameworkDocs(files, packageName) {
|
|
612
|
+
if (!packageName) return files;
|
|
613
|
+
const shortName = packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "");
|
|
614
|
+
const targetFramework = [...FRAMEWORK_NAMES].find((fw) => shortName.includes(fw));
|
|
615
|
+
if (!targetFramework) return files;
|
|
616
|
+
const otherFrameworks = [...FRAMEWORK_NAMES].filter((fw) => fw !== targetFramework);
|
|
617
|
+
const excludePattern = new RegExp(`\\b(?:${otherFrameworks.join("|")})\\b`);
|
|
618
|
+
return files.filter((f) => !excludePattern.test(f));
|
|
619
|
+
}
|
|
620
|
+
const NOISE_PATTERNS = [
|
|
621
|
+
/^\.changeset\//,
|
|
622
|
+
/CHANGELOG\.md$/i,
|
|
623
|
+
/CONTRIBUTING\.md$/i,
|
|
624
|
+
/^\.github\//
|
|
625
|
+
];
|
|
626
|
+
const EXCLUDE_DIRS = new Set([
|
|
627
|
+
"test",
|
|
628
|
+
"tests",
|
|
629
|
+
"__tests__",
|
|
630
|
+
"fixtures",
|
|
631
|
+
"fixture",
|
|
632
|
+
"examples",
|
|
633
|
+
"example",
|
|
634
|
+
"node_modules",
|
|
635
|
+
".git",
|
|
636
|
+
"dist",
|
|
637
|
+
"build",
|
|
638
|
+
"coverage",
|
|
639
|
+
"e2e",
|
|
640
|
+
"spec",
|
|
641
|
+
"mocks",
|
|
642
|
+
"__mocks__"
|
|
643
|
+
]);
|
|
644
|
+
const DOC_DIR_BONUS = new Set([
|
|
645
|
+
"docs",
|
|
646
|
+
"documentation",
|
|
647
|
+
"pages",
|
|
648
|
+
"content",
|
|
649
|
+
"website",
|
|
650
|
+
"guide",
|
|
651
|
+
"guides",
|
|
652
|
+
"wiki",
|
|
653
|
+
"manual",
|
|
654
|
+
"api"
|
|
655
|
+
]);
|
|
656
|
+
function hasExcludedDir(path) {
|
|
657
|
+
return path.split("/").some((p) => EXCLUDE_DIRS.has(p.toLowerCase()));
|
|
658
|
+
}
|
|
659
|
+
function getPathDepth(path) {
|
|
660
|
+
return path.split("/").filter(Boolean).length;
|
|
661
|
+
}
|
|
662
|
+
function hasDocDirBonus(path) {
|
|
663
|
+
return path.split("/").some((p) => DOC_DIR_BONUS.has(p.toLowerCase()));
|
|
664
|
+
}
|
|
665
|
+
function scoreDocDir(dir, fileCount) {
|
|
666
|
+
const depth = getPathDepth(dir) || 1;
|
|
667
|
+
return fileCount * (hasDocDirBonus(dir) ? 1.5 : 1) / depth;
|
|
668
|
+
}
|
|
669
|
+
function discoverDocFiles(allFiles, packageName) {
|
|
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("/"));
|
|
671
|
+
if (packageName?.includes("/")) {
|
|
672
|
+
const subPkgPrefix = `packages/${packageName.split("/").pop().toLowerCase()}/`;
|
|
673
|
+
const subPkgFiles = mdFiles.filter((f) => f.startsWith(subPkgPrefix));
|
|
674
|
+
if (subPkgFiles.length >= 3) return {
|
|
675
|
+
files: subPkgFiles,
|
|
676
|
+
prefix: subPkgPrefix
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const docsGroups = /* @__PURE__ */ new Map();
|
|
680
|
+
for (const file of mdFiles) {
|
|
681
|
+
const docsIdx = file.lastIndexOf("/docs/");
|
|
682
|
+
if (docsIdx === -1) continue;
|
|
683
|
+
mapInsert(docsGroups, file.slice(0, docsIdx + 6), () => []).push(file);
|
|
684
|
+
}
|
|
685
|
+
if (docsGroups.size > 0) {
|
|
686
|
+
const largest = [...docsGroups.entries()].sort((a, b) => b[1].length - a[1].length)[0];
|
|
687
|
+
if (largest[1].length >= 3) {
|
|
688
|
+
const fullPrefix = largest[0];
|
|
689
|
+
const docsIdx = fullPrefix.lastIndexOf("docs/");
|
|
690
|
+
const stripPrefix = docsIdx > 0 ? fullPrefix.slice(0, docsIdx) : "";
|
|
691
|
+
return {
|
|
692
|
+
files: largest[1],
|
|
693
|
+
prefix: stripPrefix
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const dirGroups = /* @__PURE__ */ new Map();
|
|
698
|
+
for (const file of mdFiles) {
|
|
699
|
+
if (hasExcludedDir(file)) continue;
|
|
700
|
+
const lastSlash = file.lastIndexOf("/");
|
|
701
|
+
if (lastSlash === -1) continue;
|
|
702
|
+
mapInsert(dirGroups, file.slice(0, lastSlash + 1), () => []).push(file);
|
|
703
|
+
}
|
|
704
|
+
if (dirGroups.size === 0) return null;
|
|
705
|
+
const scored = Array.from(dirGroups.entries(), ([dir, files]) => ({
|
|
706
|
+
dir,
|
|
707
|
+
files,
|
|
708
|
+
score: scoreDocDir(dir, files.length)
|
|
709
|
+
})).filter((d) => d.files.length >= 5).sort((a, b) => b.score - a.score);
|
|
710
|
+
if (scored.length === 0) return null;
|
|
711
|
+
const best = scored[0];
|
|
712
|
+
return {
|
|
713
|
+
files: best.files,
|
|
714
|
+
prefix: best.dir
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
async function listDocsAtRef(owner, repo, ref, pathPrefix = "docs/") {
|
|
718
|
+
return filterDocFiles(await listFilesAtRef(owner, repo, ref), pathPrefix);
|
|
719
|
+
}
|
|
720
|
+
async function fetchGitDocs(owner, repo, version, packageName, repoUrl) {
|
|
721
|
+
const override = packageName ? getDocOverride(packageName) : void 0;
|
|
722
|
+
if (override) {
|
|
723
|
+
const ref = override.ref || "main";
|
|
724
|
+
const fallback = !override.ref;
|
|
725
|
+
const files = await listDocsAtRef(override.owner, override.repo, ref, `${override.path}/`);
|
|
726
|
+
if (files.length === 0) return null;
|
|
727
|
+
return {
|
|
728
|
+
baseUrl: `https://raw.githubusercontent.com/${override.owner}/${override.repo}/${ref}`,
|
|
729
|
+
ref,
|
|
730
|
+
files,
|
|
731
|
+
fallback,
|
|
732
|
+
docsPrefix: `${override.path}/` !== "docs/" ? `${override.path}/` : void 0
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const tag = await findGitTag(owner, repo, version, packageName, repoUrl ? extractBranchHint(repoUrl) : void 0);
|
|
736
|
+
if (!tag) return null;
|
|
737
|
+
let docs = filterDocFiles(tag.files, "docs/");
|
|
738
|
+
let docsPrefix;
|
|
739
|
+
let allFiles;
|
|
740
|
+
if (docs.length === 0) {
|
|
741
|
+
const discovered = discoverDocFiles(tag.files, packageName);
|
|
742
|
+
if (discovered) {
|
|
743
|
+
docs = discovered.files;
|
|
744
|
+
docsPrefix = discovered.prefix || void 0;
|
|
745
|
+
allFiles = tag.files;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
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;
|
|
1121
|
+
return {
|
|
1122
|
+
raw: content,
|
|
1123
|
+
links: parseMarkdownLinks(content)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function parseMarkdownLinks(content) {
|
|
1127
|
+
return extractLinks(content).filter((l) => l.url.endsWith(".md"));
|
|
1128
|
+
}
|
|
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)");
|
|
1150
|
+
}
|
|
1151
|
+
normalized = normalized.replace(/\]\(\/([^)]+\.md)\)/g, "](./docs/$1)");
|
|
1152
|
+
return normalized;
|
|
1153
|
+
}
|
|
1154
|
+
async function verifyNpmRepo(owner, repo, packageName) {
|
|
1155
|
+
const base = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD`;
|
|
1156
|
+
const paths = [
|
|
1157
|
+
"package.json",
|
|
1158
|
+
`packages/${packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "")}/package.json`,
|
|
1159
|
+
`packages/${packageName.replace(NPM_SCOPE_PREFIX_RE, "").replace("/", "-")}/package.json`
|
|
1160
|
+
];
|
|
1161
|
+
for (const path of paths) {
|
|
1162
|
+
const text = await fetchGitHubRaw(`${base}/${path}`);
|
|
1163
|
+
if (!text) continue;
|
|
1164
|
+
try {
|
|
1165
|
+
if (JSON.parse(text).name === packageName) return true;
|
|
1166
|
+
} catch {}
|
|
1167
|
+
}
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
async function searchGitHubRepo(packageName) {
|
|
1171
|
+
const shortName = packageName.replace(NPM_SCOPE_WITH_SLASH_RE, "");
|
|
1172
|
+
for (const candidate of [packageName.replace(NPM_SCOPE_PREFIX_RE, "").replace("/", "/"), shortName]) {
|
|
1173
|
+
if (!candidate.includes("/")) {
|
|
1174
|
+
if ((await $fetch.raw(`https://ungh.cc/repos/${shortName}/${shortName}`).catch(() => null))?.ok) return `https://github.com/${shortName}/${shortName}`;
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if ((await $fetch.raw(`https://ungh.cc/repos/${candidate}`).catch(() => null))?.ok) return `https://github.com/${candidate}`;
|
|
1178
|
+
}
|
|
1179
|
+
const searchTerm = packageName.replace(NPM_SCOPE_PREFIX_RE, "");
|
|
1180
|
+
if (isGhAvailable()) try {
|
|
1181
|
+
const { stdout: json } = spawnSync("gh", [
|
|
1182
|
+
"search",
|
|
1183
|
+
"repos",
|
|
1184
|
+
searchTerm,
|
|
1185
|
+
"--json",
|
|
1186
|
+
"fullName",
|
|
1187
|
+
"--limit",
|
|
1188
|
+
"5"
|
|
1189
|
+
], {
|
|
1190
|
+
encoding: "utf-8",
|
|
1191
|
+
timeout: 15e3
|
|
1192
|
+
});
|
|
1193
|
+
if (!json) throw new Error("no output");
|
|
1194
|
+
const repos = JSON.parse(json);
|
|
1195
|
+
const match = repos.find((r) => r.fullName.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.fullName.toLowerCase().endsWith(`/${shortName.toLowerCase()}`));
|
|
1196
|
+
if (match) return `https://github.com/${match.fullName}`;
|
|
1197
|
+
for (const candidate of repos) {
|
|
1198
|
+
const gh = parseGitHubUrl(`https://github.com/${candidate.fullName}`);
|
|
1199
|
+
if (gh && await verifyNpmRepo(gh.owner, gh.repo, packageName)) return `https://github.com/${candidate.fullName}`;
|
|
1200
|
+
}
|
|
1201
|
+
} catch {}
|
|
1202
|
+
const data = await $fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchTerm} in:name`)}&per_page=5`).catch(() => null);
|
|
1203
|
+
if (!data?.items?.length) return null;
|
|
1204
|
+
const match = data.items.find((r) => r.full_name.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.full_name.toLowerCase().endsWith(`/${shortName.toLowerCase()}`));
|
|
1205
|
+
if (match) return `https://github.com/${match.full_name}`;
|
|
1206
|
+
for (const candidate of data.items) {
|
|
1207
|
+
const gh = parseGitHubUrl(`https://github.com/${candidate.full_name}`);
|
|
1208
|
+
if (gh && await verifyNpmRepo(gh.owner, gh.repo, packageName)) return `https://github.com/${candidate.full_name}`;
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
async function fetchGitHubRepoMeta(owner, repo, packageName) {
|
|
1213
|
+
const override = packageName ? getDocOverride(packageName) : void 0;
|
|
1214
|
+
if (override?.homepage) return { homepage: override.homepage };
|
|
1215
|
+
const data = await ghApi(`repos/${owner}/${repo}`) ?? await $fetch(`https://api.github.com/repos/${owner}/${repo}`).catch(() => null);
|
|
1216
|
+
return data?.homepage ? { homepage: data.homepage } : null;
|
|
1217
|
+
}
|
|
1218
|
+
async function fetchReadme(owner, repo, subdir, ref) {
|
|
1219
|
+
const branch = ref || "main";
|
|
1220
|
+
if (!isKnownPrivateRepo(owner, repo)) {
|
|
1221
|
+
const unghUrl = subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/${branch}/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme${ref ? `?ref=${ref}` : ""}`;
|
|
1222
|
+
if ((await $fetch.raw(unghUrl).catch(() => null))?.ok) return `ungh://${owner}/${repo}${subdir ? `/${subdir}` : ""}${ref ? `@${ref}` : ""}`;
|
|
1223
|
+
}
|
|
1224
|
+
const basePath = subdir ? `${subdir}/` : "";
|
|
1225
|
+
const branches = ref ? [ref] : ["main", "master"];
|
|
1226
|
+
const token = isKnownPrivateRepo(owner, repo) ? getGitHubToken() : null;
|
|
1227
|
+
const authHeaders = token ? { Authorization: `token ${token}` } : {};
|
|
1228
|
+
for (const b of branches) for (const filename of [
|
|
1229
|
+
"README.md",
|
|
1230
|
+
"Readme.md",
|
|
1231
|
+
"readme.md"
|
|
1232
|
+
]) {
|
|
1233
|
+
const readmeUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${b}/${basePath}${filename}`;
|
|
1234
|
+
if ((await $fetch.raw(readmeUrl, { headers: authHeaders }).catch(() => null))?.ok) return readmeUrl;
|
|
1235
|
+
}
|
|
1236
|
+
const refParam = ref ? `?ref=${ref}` : "";
|
|
1237
|
+
const apiData = await ghApi(subdir ? `repos/${owner}/${repo}/contents/${subdir}/README.md${refParam}` : `repos/${owner}/${repo}/readme${refParam}`);
|
|
1238
|
+
if (apiData?.download_url) {
|
|
1239
|
+
markRepoPrivate(owner, repo);
|
|
1240
|
+
return apiData.download_url;
|
|
1241
|
+
}
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
async function fetchReadmeContent(url) {
|
|
1245
|
+
if (url.startsWith("file://")) {
|
|
1246
|
+
const filePath = fileURLToPath(url);
|
|
1247
|
+
if (!existsSync(filePath)) return null;
|
|
1248
|
+
return readFileSync(filePath, "utf-8");
|
|
1249
|
+
}
|
|
1250
|
+
if (url.startsWith("ungh://")) {
|
|
1251
|
+
let path = url.replace("ungh://", "");
|
|
1252
|
+
let ref = "main";
|
|
1253
|
+
const atIdx = path.lastIndexOf("@");
|
|
1254
|
+
if (atIdx !== -1) {
|
|
1255
|
+
ref = path.slice(atIdx + 1);
|
|
1256
|
+
path = path.slice(0, atIdx);
|
|
1257
|
+
}
|
|
1258
|
+
const parts = path.split("/");
|
|
1259
|
+
const owner = parts[0];
|
|
1260
|
+
const repo = parts[1];
|
|
1261
|
+
const subdir = parts.slice(2).join("/");
|
|
1262
|
+
const text = await $fetch(subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/${ref}/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme?ref=${ref}`, { responseType: "text" }).catch(() => null);
|
|
1263
|
+
if (!text) return null;
|
|
1264
|
+
try {
|
|
1265
|
+
const json = JSON.parse(text);
|
|
1266
|
+
return json.markdown || json.file?.contents || null;
|
|
1267
|
+
} catch {
|
|
1268
|
+
return text;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (url.includes("raw.githubusercontent.com")) return fetchGitHubRaw(url);
|
|
1272
|
+
return fetchText(url);
|
|
1273
|
+
}
|
|
1274
|
+
async function resolveGitHubRepo(owner, repo, onProgress) {
|
|
1275
|
+
onProgress?.("Fetching repo metadata");
|
|
1276
|
+
const repoUrl = `https://github.com/${owner}/${repo}`;
|
|
1277
|
+
const meta = await ghApi(`repos/${owner}/${repo}`) ?? await $fetch(`https://api.github.com/repos/${owner}/${repo}`).catch(() => null);
|
|
1278
|
+
const homepage = meta?.homepage || void 0;
|
|
1279
|
+
const description = meta?.description || void 0;
|
|
1280
|
+
onProgress?.("Fetching latest release");
|
|
1281
|
+
const releases = await fetchUnghReleases(owner, repo);
|
|
1282
|
+
let version = "main";
|
|
1283
|
+
let releasedAt;
|
|
1284
|
+
const latestRelease = releases[0];
|
|
1285
|
+
if (latestRelease) {
|
|
1286
|
+
version = latestRelease.tag.replace(V_PREFIX_RE, "");
|
|
1287
|
+
releasedAt = latestRelease.publishedAt;
|
|
1288
|
+
}
|
|
1289
|
+
onProgress?.("Resolving docs");
|
|
1290
|
+
const gitDocs = await fetchGitDocs(owner, repo, version);
|
|
1291
|
+
const gitDocsUrl = gitDocs ? `${repoUrl}/tree/${gitDocs.ref}/docs` : void 0;
|
|
1292
|
+
const gitRef = gitDocs?.ref;
|
|
1293
|
+
onProgress?.("Fetching README");
|
|
1294
|
+
const readmeUrl = await fetchReadme(owner, repo);
|
|
1295
|
+
let llmsUrl;
|
|
1296
|
+
if (homepage) {
|
|
1297
|
+
onProgress?.("Checking llms.txt");
|
|
1298
|
+
llmsUrl = await fetchLlmsUrl(homepage).catch(() => null) ?? void 0;
|
|
1299
|
+
}
|
|
1300
|
+
if (!gitDocsUrl && !readmeUrl && !llmsUrl) return null;
|
|
1301
|
+
return {
|
|
1302
|
+
name: repo,
|
|
1303
|
+
version: latestRelease ? version : void 0,
|
|
1304
|
+
releasedAt,
|
|
1305
|
+
description,
|
|
1306
|
+
repoUrl,
|
|
1307
|
+
docsUrl: homepage,
|
|
1308
|
+
gitDocsUrl,
|
|
1309
|
+
gitRef,
|
|
1310
|
+
gitDocsFallback: gitDocs?.fallback,
|
|
1311
|
+
readmeUrl: readmeUrl ?? void 0,
|
|
1312
|
+
llmsUrl
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
const VALID_CRATE_NAME = /^[a-z0-9][\w-]*$/;
|
|
1316
|
+
const runCratesApiRateLimited = createRateLimitedRunner(1e3);
|
|
1317
|
+
function selectCrateVersion(data, requestedVersion) {
|
|
1318
|
+
const versions = data.versions || [];
|
|
1319
|
+
if (requestedVersion) {
|
|
1320
|
+
const exact = versions.find((v) => v.num === requestedVersion && !v.yanked);
|
|
1321
|
+
if (exact?.num) return {
|
|
1322
|
+
version: exact.num,
|
|
1323
|
+
entry: exact
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
const crate = data.crate;
|
|
1327
|
+
const preferred = [
|
|
1328
|
+
crate?.max_stable_version,
|
|
1329
|
+
crate?.newest_version,
|
|
1330
|
+
crate?.max_version,
|
|
1331
|
+
crate?.default_version
|
|
1332
|
+
].find(Boolean);
|
|
1333
|
+
if (preferred) {
|
|
1334
|
+
const match = versions.find((v) => v.num === preferred && !v.yanked);
|
|
1335
|
+
if (match?.num) return {
|
|
1336
|
+
version: preferred,
|
|
1337
|
+
entry: match
|
|
1338
|
+
};
|
|
1339
|
+
if (versions.length === 0) return { version: preferred };
|
|
1340
|
+
}
|
|
1341
|
+
const firstStable = versions.find((v) => !v.yanked && v.num);
|
|
1342
|
+
if (firstStable?.num) return {
|
|
1343
|
+
version: firstStable.num,
|
|
1344
|
+
entry: firstStable
|
|
1345
|
+
};
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
function pickPreferredUrl(...urls) {
|
|
1349
|
+
return urls.map((v) => v?.trim()).find((v) => !!v);
|
|
1350
|
+
}
|
|
1351
|
+
async function fetchCratesApi(url) {
|
|
1352
|
+
return runCratesApiRateLimited(() => $fetch(url).catch(() => null));
|
|
1353
|
+
}
|
|
1354
|
+
async function resolveCrateDocsWithAttempts(crateName, options = {}) {
|
|
1355
|
+
const attempts = [];
|
|
1356
|
+
const onProgress = options.onProgress;
|
|
1357
|
+
const normalizedName = crateName.trim().toLowerCase();
|
|
1358
|
+
if (!normalizedName || !VALID_CRATE_NAME.test(normalizedName)) {
|
|
1359
|
+
attempts.push({
|
|
1360
|
+
source: "crates",
|
|
1361
|
+
status: "error",
|
|
1362
|
+
message: `Invalid crate name: ${crateName}`
|
|
1363
|
+
});
|
|
1364
|
+
return {
|
|
1365
|
+
package: null,
|
|
1366
|
+
attempts
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
onProgress?.("crates.io metadata");
|
|
1370
|
+
const apiUrl = `https://crates.io/api/v1/crates/${encodeURIComponent(normalizedName)}`;
|
|
1371
|
+
const data = await fetchCratesApi(apiUrl);
|
|
1372
|
+
if (!data?.crate) {
|
|
1373
|
+
attempts.push({
|
|
1374
|
+
source: "crates",
|
|
1375
|
+
url: apiUrl,
|
|
1376
|
+
status: "not-found",
|
|
1377
|
+
message: "Crate not found on crates.io"
|
|
1378
|
+
});
|
|
1379
|
+
return {
|
|
1380
|
+
package: null,
|
|
1381
|
+
attempts
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
attempts.push({
|
|
1385
|
+
source: "crates",
|
|
1386
|
+
url: apiUrl,
|
|
1387
|
+
status: "success",
|
|
1388
|
+
message: `Found crate: ${data.crate.name || normalizedName}`
|
|
1389
|
+
});
|
|
1390
|
+
const selected = selectCrateVersion(data, options.version);
|
|
1391
|
+
if (!selected) {
|
|
1392
|
+
attempts.push({
|
|
1393
|
+
source: "crates",
|
|
1394
|
+
url: apiUrl,
|
|
1395
|
+
status: "error",
|
|
1396
|
+
message: "No usable crate versions found"
|
|
1397
|
+
});
|
|
1398
|
+
return {
|
|
1399
|
+
package: null,
|
|
1400
|
+
attempts
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
const version = selected.version;
|
|
1404
|
+
const versionEntry = selected.entry;
|
|
1405
|
+
const docsRsUrl = `https://docs.rs/${encodeURIComponent(normalizedName)}/${encodeURIComponent(version)}`;
|
|
1406
|
+
const repositoryRaw = pickPreferredUrl(versionEntry?.repository, data.crate.repository);
|
|
1407
|
+
const homepage = pickPreferredUrl(versionEntry?.homepage, data.crate.homepage);
|
|
1408
|
+
const documentation = pickPreferredUrl(versionEntry?.documentation, data.crate.documentation);
|
|
1409
|
+
const normalizedRepo = repositoryRaw ? normalizeRepoUrl(repositoryRaw) : void 0;
|
|
1410
|
+
const repoUrl = normalizedRepo && isLikelyCodeHostUrl(normalizedRepo) ? normalizedRepo : isLikelyCodeHostUrl(homepage) ? homepage : void 0;
|
|
1411
|
+
let resolved = {
|
|
1412
|
+
name: normalizedName,
|
|
1413
|
+
version,
|
|
1414
|
+
releasedAt: versionEntry?.created_at || data.crate.updated_at || void 0,
|
|
1415
|
+
description: versionEntry?.description || data.crate.description,
|
|
1416
|
+
docsUrl: (() => {
|
|
1417
|
+
if (documentation && !isUselessDocsUrl(documentation) && !isLikelyCodeHostUrl(documentation)) return documentation;
|
|
1418
|
+
if (homepage && !isUselessDocsUrl(homepage) && !isLikelyCodeHostUrl(homepage)) return homepage;
|
|
1419
|
+
return docsRsUrl;
|
|
1420
|
+
})(),
|
|
1421
|
+
repoUrl
|
|
1422
|
+
};
|
|
1423
|
+
const gh = repoUrl ? parseGitHubUrl(repoUrl) : null;
|
|
1424
|
+
if (gh) {
|
|
1425
|
+
onProgress?.("GitHub enrichment");
|
|
1426
|
+
const ghResolved = await resolveGitHubRepo(gh.owner, gh.repo);
|
|
1427
|
+
if (ghResolved) {
|
|
1428
|
+
attempts.push({
|
|
1429
|
+
source: "github-meta",
|
|
1430
|
+
url: repoUrl,
|
|
1431
|
+
status: "success",
|
|
1432
|
+
message: "Enriched via GitHub repo metadata"
|
|
1433
|
+
});
|
|
1434
|
+
resolved = {
|
|
1435
|
+
...ghResolved,
|
|
1436
|
+
name: normalizedName,
|
|
1437
|
+
version,
|
|
1438
|
+
releasedAt: resolved.releasedAt || ghResolved.releasedAt,
|
|
1439
|
+
description: resolved.description || ghResolved.description,
|
|
1440
|
+
docsUrl: resolved.docsUrl || ghResolved.docsUrl,
|
|
1441
|
+
repoUrl,
|
|
1442
|
+
readmeUrl: ghResolved.readmeUrl || resolved.readmeUrl
|
|
1443
|
+
};
|
|
1444
|
+
} else attempts.push({
|
|
1445
|
+
source: "github-meta",
|
|
1446
|
+
url: repoUrl,
|
|
1447
|
+
status: "not-found",
|
|
1448
|
+
message: "GitHub enrichment failed, using crates.io metadata"
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
if (!resolved.llmsUrl && resolved.docsUrl) {
|
|
1452
|
+
onProgress?.("llms.txt discovery");
|
|
1453
|
+
resolved.llmsUrl = await fetchLlmsUrl(resolved.docsUrl).catch(() => null) ?? void 0;
|
|
1454
|
+
if (resolved.llmsUrl) attempts.push({
|
|
1455
|
+
source: "llms.txt",
|
|
1456
|
+
url: resolved.llmsUrl,
|
|
1457
|
+
status: "success"
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
package: resolved,
|
|
1462
|
+
attempts
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
const STATIC_REGEX_2 = /[_.:-]/;
|
|
1466
|
+
const STATIC_REGEX_3 = /\/+$/;
|
|
1467
|
+
async function fetchCrawledDocs(url, onProgress, maxPages = 200) {
|
|
1468
|
+
const outputDir = join(tmpdir(), "skilld-crawl", Date.now().toString());
|
|
1469
|
+
onProgress?.(`Crawling ${url}`);
|
|
1470
|
+
const userLang = getUserLang();
|
|
1471
|
+
const foreignUrls = /* @__PURE__ */ new Set();
|
|
1472
|
+
const doCrawl = () => crawlAndGenerate({
|
|
1473
|
+
urls: [url],
|
|
1474
|
+
outputDir,
|
|
1475
|
+
driver: "http",
|
|
1476
|
+
generateLlmsTxt: false,
|
|
1477
|
+
generateIndividualMd: true,
|
|
1478
|
+
maxRequestsPerCrawl: maxPages,
|
|
1479
|
+
onPage: (page) => {
|
|
1480
|
+
const lang = extractHtmlLang(page.html);
|
|
1481
|
+
if (lang && !lang.startsWith("en") && !lang.startsWith(userLang)) foreignUrls.add(page.url);
|
|
1482
|
+
}
|
|
1483
|
+
}, (progress) => {
|
|
1484
|
+
if (progress.crawling.status === "processing" && progress.crawling.total > 0) onProgress?.(`Crawling ${progress.crawling.processed}/${progress.crawling.total} pages`);
|
|
1485
|
+
});
|
|
1486
|
+
let results = await doCrawl().catch((err) => {
|
|
1487
|
+
onProgress?.(`Crawl failed: ${err?.message || err}`);
|
|
1488
|
+
return [];
|
|
1489
|
+
});
|
|
1490
|
+
if (results.length === 0) {
|
|
1491
|
+
onProgress?.("Retrying crawl");
|
|
1492
|
+
results = await doCrawl().catch(() => []);
|
|
1493
|
+
}
|
|
1494
|
+
rmSync(outputDir, {
|
|
1495
|
+
recursive: true,
|
|
1496
|
+
force: true
|
|
1497
|
+
});
|
|
1498
|
+
const docs = [];
|
|
1499
|
+
let localeFiltered = 0;
|
|
1500
|
+
for (const result of results) {
|
|
1501
|
+
if (!result.success || !result.content) continue;
|
|
1502
|
+
if (foreignUrls.has(result.url)) {
|
|
1503
|
+
localeFiltered++;
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const segments = (new URL(result.url).pathname.replace(TRAILING_SLASH_RE, "") || "/index").split("/").filter(Boolean);
|
|
1507
|
+
if (isForeignPathPrefix(segments[0], userLang)) {
|
|
1508
|
+
localeFiltered++;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const path = `docs/${segments.join("/")}.md`;
|
|
1512
|
+
docs.push({
|
|
1513
|
+
path,
|
|
1514
|
+
content: result.content
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
if (localeFiltered > 0) onProgress?.(`Filtered ${localeFiltered} foreign locale pages`);
|
|
1518
|
+
onProgress?.(`Crawled ${docs.length} pages`);
|
|
1519
|
+
return docs;
|
|
1520
|
+
}
|
|
1521
|
+
const HTML_LANG_RE = /<html[^>]*\slang=["']([^"']+)["']/i;
|
|
1522
|
+
function extractHtmlLang(html) {
|
|
1523
|
+
return HTML_LANG_RE.exec(html)?.[1]?.toLowerCase();
|
|
1524
|
+
}
|
|
1525
|
+
const LOCALE_CODES = new Set([
|
|
1526
|
+
"ar",
|
|
1527
|
+
"de",
|
|
1528
|
+
"es",
|
|
1529
|
+
"fr",
|
|
1530
|
+
"id",
|
|
1531
|
+
"it",
|
|
1532
|
+
"ja",
|
|
1533
|
+
"ko",
|
|
1534
|
+
"nl",
|
|
1535
|
+
"pl",
|
|
1536
|
+
"pt",
|
|
1537
|
+
"pt-br",
|
|
1538
|
+
"ru",
|
|
1539
|
+
"th",
|
|
1540
|
+
"tr",
|
|
1541
|
+
"uk",
|
|
1542
|
+
"vi",
|
|
1543
|
+
"zh",
|
|
1544
|
+
"zh-cn",
|
|
1545
|
+
"zh-tw"
|
|
1546
|
+
]);
|
|
1547
|
+
function isForeignPathPrefix(segment, userLang) {
|
|
1548
|
+
if (!segment) return false;
|
|
1549
|
+
const lower = segment.toLowerCase();
|
|
1550
|
+
if (lower === "en" || lower.startsWith(userLang)) return false;
|
|
1551
|
+
return LOCALE_CODES.has(lower);
|
|
1552
|
+
}
|
|
1553
|
+
function getUserLang() {
|
|
1554
|
+
const code = (process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || "").split(STATIC_REGEX_2)[0]?.toLowerCase() || "";
|
|
1555
|
+
return code.length >= 2 ? code.slice(0, 2) : "en";
|
|
1556
|
+
}
|
|
1557
|
+
function toCrawlPattern(docsUrl) {
|
|
1558
|
+
return `${docsUrl.replace(STATIC_REGEX_3, "")}/**`;
|
|
1559
|
+
}
|
|
1560
|
+
const HIGH_VALUE_CATEGORIES = new Set([
|
|
1561
|
+
"q&a",
|
|
1562
|
+
"help",
|
|
1563
|
+
"troubleshooting",
|
|
1564
|
+
"support"
|
|
1565
|
+
]);
|
|
1566
|
+
const LOW_VALUE_CATEGORIES = new Set([
|
|
1567
|
+
"show and tell",
|
|
1568
|
+
"ideas",
|
|
1569
|
+
"polls"
|
|
1570
|
+
]);
|
|
1571
|
+
const TITLE_NOISE_RE = /looking .*(?:developer|engineer|freelanc)|hiring|job post|guide me to (?:complete|finish|build)|help me (?:complete|finish|build)|seeking .* tutorial|recommend.* course/i;
|
|
1572
|
+
const MIN_DISCUSSION_SCORE = 3;
|
|
1573
|
+
function scoreComment(c) {
|
|
1574
|
+
return (c.isMaintainer ? 3 : 1) * (hasCodeBlock(c.body) ? 2 : 1) * (1 + c.reactions);
|
|
1575
|
+
}
|
|
1576
|
+
function scoreDiscussion(d) {
|
|
1577
|
+
if (TITLE_NOISE_RE.test(d.title)) return -1;
|
|
1578
|
+
let score = 0;
|
|
1579
|
+
if (d.isMaintainer) score += 3;
|
|
1580
|
+
if (hasCodeBlock([
|
|
1581
|
+
d.body,
|
|
1582
|
+
d.answer || "",
|
|
1583
|
+
...d.topComments.map((c) => c.body)
|
|
1584
|
+
].join("\n"))) score += 3;
|
|
1585
|
+
score += Math.min(d.upvoteCount, 5);
|
|
1586
|
+
if (d.answer) {
|
|
1587
|
+
score += 2;
|
|
1588
|
+
if (d.answer.length > 100) score += 1;
|
|
1589
|
+
}
|
|
1590
|
+
if (d.topComments.some((c) => c.isMaintainer)) score += 2;
|
|
1591
|
+
if (d.topComments.some((c) => c.reactions > 0)) score += 1;
|
|
1592
|
+
return score;
|
|
1593
|
+
}
|
|
1594
|
+
async function fetchGitHubDiscussions(owner, repo, limit = 20, releasedAt, fromDate) {
|
|
1595
|
+
if (!isGhAvailable()) return [];
|
|
1596
|
+
if (!fromDate && releasedAt) {
|
|
1597
|
+
const cutoff = new Date(releasedAt);
|
|
1598
|
+
cutoff.setMonth(cutoff.getMonth() + 6);
|
|
1599
|
+
if (cutoff < /* @__PURE__ */ new Date()) return [];
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
const { stdout: result } = spawnSync("gh", [
|
|
1603
|
+
"api",
|
|
1604
|
+
"graphql",
|
|
1605
|
+
"-f",
|
|
1606
|
+
`query=${`query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { discussions(first: ${Math.min(limit * 3, 80)}, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number title body category { name } createdAt url upvoteCount comments(first: 10) { totalCount nodes { body author { login } authorAssociation reactions { totalCount } } } answer { body author { login } authorAssociation } author { login } authorAssociation } } } }`}`,
|
|
1607
|
+
"-f",
|
|
1608
|
+
`owner=${owner}`,
|
|
1609
|
+
"-f",
|
|
1610
|
+
`repo=${repo}`
|
|
1611
|
+
], {
|
|
1612
|
+
encoding: "utf-8",
|
|
1613
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1614
|
+
});
|
|
1615
|
+
if (!result) return [];
|
|
1616
|
+
const nodes = JSON.parse(result)?.data?.repository?.discussions?.nodes;
|
|
1617
|
+
if (!Array.isArray(nodes)) return [];
|
|
1618
|
+
const fromTs = fromDate ? new Date(fromDate).getTime() : null;
|
|
1619
|
+
return nodes.filter((d) => d.author && !BOT_USERS.has(d.author.login)).filter((d) => {
|
|
1620
|
+
const cat = (d.category?.name || "").toLowerCase();
|
|
1621
|
+
return !LOW_VALUE_CATEGORIES.has(cat);
|
|
1622
|
+
}).filter((d) => !fromTs || new Date(d.createdAt).getTime() >= fromTs).map((d) => {
|
|
1623
|
+
let answer;
|
|
1624
|
+
if (d.answer?.body) {
|
|
1625
|
+
const isMaintainer = [
|
|
1626
|
+
"OWNER",
|
|
1627
|
+
"MEMBER",
|
|
1628
|
+
"COLLABORATOR"
|
|
1629
|
+
].includes(d.answer.authorAssociation);
|
|
1630
|
+
const author = d.answer.author?.login;
|
|
1631
|
+
answer = `${isMaintainer && author ? `**@${author}** [maintainer]:\n\n` : ""}${d.answer.body}`;
|
|
1632
|
+
}
|
|
1633
|
+
const comments = (d.comments?.nodes || []).filter((c) => c.author && !BOT_USERS.has(c.author.login)).filter((c) => !COMMENT_NOISE_RE.test((c.body || "").trim())).map((c) => {
|
|
1634
|
+
const isMaintainer = [
|
|
1635
|
+
"OWNER",
|
|
1636
|
+
"MEMBER",
|
|
1637
|
+
"COLLABORATOR"
|
|
1638
|
+
].includes(c.authorAssociation);
|
|
1639
|
+
return {
|
|
1640
|
+
body: c.body || "",
|
|
1641
|
+
author: c.author.login,
|
|
1642
|
+
reactions: c.reactions?.totalCount || 0,
|
|
1643
|
+
isMaintainer
|
|
1644
|
+
};
|
|
1645
|
+
}).sort((a, b) => scoreComment(b) - scoreComment(a)).slice(0, 3);
|
|
1646
|
+
return {
|
|
1647
|
+
number: d.number,
|
|
1648
|
+
title: d.title,
|
|
1649
|
+
body: d.body || "",
|
|
1650
|
+
category: d.category?.name || "",
|
|
1651
|
+
createdAt: d.createdAt,
|
|
1652
|
+
url: d.url,
|
|
1653
|
+
upvoteCount: d.upvoteCount || 0,
|
|
1654
|
+
comments: d.comments?.totalCount || 0,
|
|
1655
|
+
isMaintainer: [
|
|
1656
|
+
"OWNER",
|
|
1657
|
+
"MEMBER",
|
|
1658
|
+
"COLLABORATOR"
|
|
1659
|
+
].includes(d.authorAssociation),
|
|
1660
|
+
answer,
|
|
1661
|
+
topComments: comments
|
|
1662
|
+
};
|
|
1663
|
+
}).map((d) => ({
|
|
1664
|
+
d,
|
|
1665
|
+
score: scoreDiscussion(d)
|
|
1666
|
+
})).filter(({ score }) => score >= MIN_DISCUSSION_SCORE).sort((a, b) => {
|
|
1667
|
+
const aHigh = HIGH_VALUE_CATEGORIES.has(a.d.category.toLowerCase()) ? 1 : 0;
|
|
1668
|
+
const bHigh = HIGH_VALUE_CATEGORIES.has(b.d.category.toLowerCase()) ? 1 : 0;
|
|
1669
|
+
if (aHigh !== bHigh) return bHigh - aHigh;
|
|
1670
|
+
return b.score - a.score;
|
|
1671
|
+
}).slice(0, limit).map(({ d }) => d);
|
|
1672
|
+
} catch {
|
|
1673
|
+
return [];
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function formatDiscussionAsMarkdown(d) {
|
|
1677
|
+
const fm = buildFrontmatter({
|
|
1678
|
+
number: d.number,
|
|
1679
|
+
title: d.title,
|
|
1680
|
+
category: d.category,
|
|
1681
|
+
created: isoDate(d.createdAt),
|
|
1682
|
+
url: d.url,
|
|
1683
|
+
upvotes: d.upvoteCount,
|
|
1684
|
+
comments: d.comments,
|
|
1685
|
+
answered: !!d.answer
|
|
1686
|
+
});
|
|
1687
|
+
const bodyLimit = d.upvoteCount >= 5 ? 1500 : 800;
|
|
1688
|
+
const lines = [
|
|
1689
|
+
fm,
|
|
1690
|
+
"",
|
|
1691
|
+
`# ${d.title}`
|
|
1692
|
+
];
|
|
1693
|
+
if (d.body) lines.push("", truncateBody(d.body, bodyLimit));
|
|
1694
|
+
if (d.answer) lines.push("", "---", "", "## Accepted Answer", "", truncateBody(d.answer, 1e3));
|
|
1695
|
+
else if (d.topComments.length > 0) {
|
|
1696
|
+
lines.push("", "---", "", "## Top Comments");
|
|
1697
|
+
for (const c of d.topComments) {
|
|
1698
|
+
const reactions = c.reactions > 0 ? ` (+${c.reactions})` : "";
|
|
1699
|
+
const maintainer = c.isMaintainer ? " [maintainer]" : "";
|
|
1700
|
+
lines.push("", `**@${c.author}**${maintainer}${reactions}:`, "", truncateBody(c.body, 600));
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return lines.join("\n");
|
|
1704
|
+
}
|
|
1705
|
+
function generateDiscussionIndex(discussions) {
|
|
1706
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1707
|
+
for (const d of discussions) mapInsert(byCategory, d.category || "Uncategorized", () => []).push(d);
|
|
1708
|
+
const answered = discussions.filter((d) => d.answer).length;
|
|
1709
|
+
const sections = [
|
|
1710
|
+
[
|
|
1711
|
+
"---",
|
|
1712
|
+
`total: ${discussions.length}`,
|
|
1713
|
+
`answered: ${answered}`,
|
|
1714
|
+
"---"
|
|
1715
|
+
].join("\n"),
|
|
1716
|
+
"",
|
|
1717
|
+
"# Discussions Index",
|
|
1718
|
+
""
|
|
1719
|
+
];
|
|
1720
|
+
const cats = [...byCategory.keys()].sort((a, b) => {
|
|
1721
|
+
return (HIGH_VALUE_CATEGORIES.has(a.toLowerCase()) ? 0 : 1) - (HIGH_VALUE_CATEGORIES.has(b.toLowerCase()) ? 0 : 1) || a.localeCompare(b);
|
|
1722
|
+
});
|
|
1723
|
+
for (const cat of cats) {
|
|
1724
|
+
const group = byCategory.get(cat);
|
|
1725
|
+
sections.push(`## ${cat} (${group.length})`, "");
|
|
1726
|
+
for (const d of group) {
|
|
1727
|
+
const upvotes = d.upvoteCount > 0 ? ` (+${d.upvoteCount})` : "";
|
|
1728
|
+
const answered = d.answer ? " [answered]" : "";
|
|
1729
|
+
const date = isoDate(d.createdAt);
|
|
1730
|
+
sections.push(`- [#${d.number}](./discussion-${d.number}.md): ${d.title}${upvotes}${answered} (${date})`);
|
|
1731
|
+
}
|
|
1732
|
+
sections.push("");
|
|
1733
|
+
}
|
|
1734
|
+
return sections.join("\n");
|
|
1735
|
+
}
|
|
1736
|
+
const STATIC_REGEX_1$4 = /\.md$/;
|
|
1737
|
+
function generateDocsIndex(docs) {
|
|
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));
|
|
1739
|
+
if (docFiles.length === 0) return "";
|
|
1740
|
+
const rootFiles = [];
|
|
1741
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
1742
|
+
for (const doc of docFiles) {
|
|
1743
|
+
const rel = doc.path.slice(5);
|
|
1744
|
+
const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "";
|
|
1745
|
+
if (!dir) rootFiles.push(doc);
|
|
1746
|
+
else {
|
|
1747
|
+
const list = byDir.get(dir);
|
|
1748
|
+
if (list) list.push(doc);
|
|
1749
|
+
else byDir.set(dir, [doc]);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const sections = [
|
|
1753
|
+
"---",
|
|
1754
|
+
`total: ${docFiles.length}`,
|
|
1755
|
+
"---",
|
|
1756
|
+
"",
|
|
1757
|
+
"# Docs Index",
|
|
1758
|
+
""
|
|
1759
|
+
];
|
|
1760
|
+
for (const file of rootFiles) {
|
|
1761
|
+
const rel = file.path.slice(5);
|
|
1762
|
+
const title = extractTitle(file.content) || rel.replace(STATIC_REGEX_1$4, "");
|
|
1763
|
+
const desc = extractDescription(file.content);
|
|
1764
|
+
const descPart = desc ? `: ${desc}` : "";
|
|
1765
|
+
sections.push(`- [${title}](./${rel})${descPart}`);
|
|
1766
|
+
}
|
|
1767
|
+
if (rootFiles.length > 0) sections.push("");
|
|
1768
|
+
for (const [dir, files] of byDir) {
|
|
1769
|
+
sections.push(`## ${dir} (${files.length})`, "");
|
|
1770
|
+
for (const file of files) {
|
|
1771
|
+
const rel = file.path.slice(5);
|
|
1772
|
+
const title = extractTitle(file.content) || rel.replace(STATIC_REGEX_1$4, "").split("/").pop();
|
|
1773
|
+
const desc = extractDescription(file.content);
|
|
1774
|
+
const descPart = desc ? `: ${desc}` : "";
|
|
1775
|
+
sections.push(`- [${title}](./${rel})${descPart}`);
|
|
1776
|
+
}
|
|
1777
|
+
sections.push("");
|
|
1778
|
+
}
|
|
1779
|
+
return sections.join("\n");
|
|
1780
|
+
}
|
|
1781
|
+
const SKIP_DIRS = [
|
|
1782
|
+
"node_modules",
|
|
1783
|
+
"_vendor",
|
|
1784
|
+
"__tests__",
|
|
1785
|
+
"__mocks__",
|
|
1786
|
+
"__fixtures__",
|
|
1787
|
+
"test",
|
|
1788
|
+
"tests",
|
|
1789
|
+
"fixture",
|
|
1790
|
+
"fixtures",
|
|
1791
|
+
"locales",
|
|
1792
|
+
"locale",
|
|
1793
|
+
"i18n",
|
|
1794
|
+
".git"
|
|
1795
|
+
];
|
|
1796
|
+
const SKIP_PATTERNS = [
|
|
1797
|
+
"*.min.*",
|
|
1798
|
+
"*.prod.*",
|
|
1799
|
+
"*.global.*",
|
|
1800
|
+
"*.browser.*",
|
|
1801
|
+
"*.map",
|
|
1802
|
+
"*.map.js",
|
|
1803
|
+
"CHANGELOG*",
|
|
1804
|
+
"LICENSE*",
|
|
1805
|
+
"README*"
|
|
1806
|
+
];
|
|
1807
|
+
const MAX_FILE_SIZE = 500 * 1024;
|
|
1808
|
+
async function resolveEntryFiles(packageDir) {
|
|
1809
|
+
if (!existsSync(join(packageDir, "package.json"))) return [];
|
|
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);
|
|
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);
|
|
1828
|
+
const entries = [];
|
|
1829
|
+
for (const file of files) {
|
|
1830
|
+
const absPath = join(packageDir, file);
|
|
1831
|
+
let content;
|
|
1832
|
+
try {
|
|
1833
|
+
content = readFileSync(absPath, "utf-8");
|
|
1834
|
+
} catch {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
if (content.length > MAX_FILE_SIZE) continue;
|
|
1838
|
+
entries.push({
|
|
1839
|
+
path: file,
|
|
1840
|
+
content,
|
|
1841
|
+
type: "types"
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
return entries;
|
|
1845
|
+
}
|
|
1846
|
+
const STATIC_REGEX_1$3 = /^[\w.-]+\/[\w.-]+$/;
|
|
1847
|
+
function parseGitSkillInput(input) {
|
|
1848
|
+
const trimmed = input.trim();
|
|
1849
|
+
if (trimmed.startsWith("@")) return null;
|
|
1850
|
+
if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/") || trimmed.startsWith("~")) return {
|
|
1851
|
+
type: "local",
|
|
1852
|
+
localPath: trimmed.startsWith("~") ? resolve(process.env.HOME || "", trimmed.slice(1)) : resolve(trimmed)
|
|
1853
|
+
};
|
|
1854
|
+
if (trimmed.startsWith("git@")) {
|
|
1855
|
+
const gh = parseGitHubUrl(normalizeRepoUrl(trimmed));
|
|
1856
|
+
if (gh) return {
|
|
1857
|
+
type: "github",
|
|
1858
|
+
owner: gh.owner,
|
|
1859
|
+
repo: gh.repo
|
|
1860
|
+
};
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
if (trimmed.startsWith("https://") || trimmed.startsWith("http://")) return parseGitUrl(trimmed);
|
|
1864
|
+
if (STATIC_REGEX_1$3.test(trimmed)) return {
|
|
1865
|
+
type: "github",
|
|
1866
|
+
owner: trimmed.split("/")[0],
|
|
1867
|
+
repo: trimmed.split("/")[1]
|
|
1868
|
+
};
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
function parseGitUrl(url) {
|
|
1872
|
+
try {
|
|
1873
|
+
const parsed = new URL(url);
|
|
1874
|
+
if (parsed.hostname === "github.com" || parsed.hostname === "www.github.com") {
|
|
1875
|
+
const parts = parsed.pathname.replace(LEADING_SLASH_RE, "").replace(GIT_SUFFIX_RE, "").split("/");
|
|
1876
|
+
const owner = parts[0];
|
|
1877
|
+
const repo = parts[1];
|
|
1878
|
+
if (!owner || !repo) return null;
|
|
1879
|
+
if (parts[2] === "tree" && parts.length >= 4) return {
|
|
1880
|
+
type: "github",
|
|
1881
|
+
owner,
|
|
1882
|
+
repo,
|
|
1883
|
+
ref: parts[3],
|
|
1884
|
+
skillPath: parts.length > 4 ? parts.slice(4).join("/") : void 0
|
|
1885
|
+
};
|
|
1886
|
+
return {
|
|
1887
|
+
type: "github",
|
|
1888
|
+
owner,
|
|
1889
|
+
repo
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
if (parsed.hostname === "gitlab.com") {
|
|
1893
|
+
const parts = parsed.pathname.replace(LEADING_SLASH_RE, "").replace(GIT_SUFFIX_RE, "").split("/");
|
|
1894
|
+
const owner = parts[0];
|
|
1895
|
+
const repo = parts[1];
|
|
1896
|
+
if (!owner || !repo) return null;
|
|
1897
|
+
return {
|
|
1898
|
+
type: "gitlab",
|
|
1899
|
+
owner,
|
|
1900
|
+
repo
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
return null;
|
|
1904
|
+
} catch {
|
|
1905
|
+
return null;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
function parseSkillFrontmatterName(content) {
|
|
1909
|
+
const fm = parseFrontmatter(content);
|
|
1910
|
+
return {
|
|
1911
|
+
name: fm.name,
|
|
1912
|
+
description: fm.description
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function findSkillDirs(root, prefix = "") {
|
|
1916
|
+
const out = [];
|
|
1917
|
+
if (!existsSync(root)) return out;
|
|
1918
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
1919
|
+
if (!entry.isDirectory()) continue;
|
|
1920
|
+
const dir = resolve(root, entry.name);
|
|
1921
|
+
const repoPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1922
|
+
if (existsSync(resolve(dir, "SKILL.md"))) out.push({
|
|
1923
|
+
dir,
|
|
1924
|
+
repoPath
|
|
1925
|
+
});
|
|
1926
|
+
else out.push(...findSkillDirs(dir, repoPath));
|
|
1927
|
+
}
|
|
1928
|
+
return out;
|
|
1929
|
+
}
|
|
1930
|
+
function collectFiles(dir, prefix = "") {
|
|
1931
|
+
const files = [];
|
|
1932
|
+
if (!existsSync(dir)) return files;
|
|
1933
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1934
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1935
|
+
const fullPath = resolve(dir, entry.name);
|
|
1936
|
+
if (entry.isDirectory()) files.push(...collectFiles(fullPath, relPath));
|
|
1937
|
+
else if (entry.isFile()) files.push({
|
|
1938
|
+
path: relPath,
|
|
1939
|
+
content: readFileSync(fullPath, "utf-8")
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
return files;
|
|
1943
|
+
}
|
|
1944
|
+
async function fetchGitSkills(source, onProgress) {
|
|
1945
|
+
if (source.type === "local") return fetchLocalSkills(source);
|
|
1946
|
+
if (source.type === "github") return fetchGitHubSkills(source, onProgress);
|
|
1947
|
+
if (source.type === "gitlab") return fetchGitLabSkills(source, onProgress);
|
|
1948
|
+
return { skills: [] };
|
|
1949
|
+
}
|
|
1950
|
+
function fetchLocalSkills(source) {
|
|
1951
|
+
const base = source.localPath;
|
|
1952
|
+
if (!existsSync(base)) return { skills: [] };
|
|
1953
|
+
const skills = [];
|
|
1954
|
+
const skillsDir = resolve(base, "skills");
|
|
1955
|
+
if (existsSync(skillsDir)) for (const { dir, repoPath } of findSkillDirs(skillsDir, "skills")) {
|
|
1956
|
+
const skill = readLocalSkill(dir, repoPath);
|
|
1957
|
+
if (skill) skills.push(skill);
|
|
1958
|
+
}
|
|
1959
|
+
if (skills.length === 0) {
|
|
1960
|
+
const skill = readLocalSkill(base, "");
|
|
1961
|
+
if (skill) skills.push(skill);
|
|
1962
|
+
}
|
|
1963
|
+
return { skills };
|
|
1964
|
+
}
|
|
1965
|
+
function readLocalSkill(dir, repoPath) {
|
|
1966
|
+
const skillMdPath = resolve(dir, "SKILL.md");
|
|
1967
|
+
if (!existsSync(skillMdPath)) return null;
|
|
1968
|
+
const content = readFileSync(skillMdPath, "utf-8");
|
|
1969
|
+
const frontmatter = parseSkillFrontmatterName(content);
|
|
1970
|
+
const dirName = dir.split("/").pop();
|
|
1971
|
+
const name = frontmatter.name || dirName;
|
|
1972
|
+
const files = collectFiles(dir).filter((f) => f.path !== "SKILL.md");
|
|
1973
|
+
return {
|
|
1974
|
+
name,
|
|
1975
|
+
description: frontmatter.description || "",
|
|
1976
|
+
path: repoPath,
|
|
1977
|
+
content,
|
|
1978
|
+
files
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
async function fetchGitHubSkills(source, onProgress) {
|
|
1982
|
+
const { owner, repo } = source;
|
|
1983
|
+
if (!owner || !repo) return { skills: [] };
|
|
1984
|
+
const ref = source.ref || "main";
|
|
1985
|
+
const refs = ref === "main" ? ["main", "master"] : [ref];
|
|
1986
|
+
for (const tryRef of refs) {
|
|
1987
|
+
const skills = await downloadGitHubSkills(owner, repo, tryRef, source.skillPath, onProgress);
|
|
1988
|
+
if (skills.length > 0) return { skills };
|
|
1989
|
+
}
|
|
1990
|
+
return { skills: [] };
|
|
1991
|
+
}
|
|
1992
|
+
async function downloadGitHubSkills(owner, repo, ref, skillPath, onProgress) {
|
|
1993
|
+
const tempDir = join(tmpdir(), `skilld-${Date.now()}`);
|
|
1994
|
+
try {
|
|
1995
|
+
if (skillPath) {
|
|
1996
|
+
onProgress?.(`Downloading ${owner}/${repo}/${skillPath}@${ref}`);
|
|
1997
|
+
const { dir } = await downloadTemplate(`github:${owner}/${repo}/${skillPath}#${ref}`, {
|
|
1998
|
+
dir: tempDir,
|
|
1999
|
+
force: true,
|
|
2000
|
+
auth: getGitHubToken() || void 0
|
|
2001
|
+
});
|
|
2002
|
+
const skill = readLocalSkill(dir, skillPath);
|
|
2003
|
+
return skill ? [skill] : [];
|
|
2004
|
+
}
|
|
2005
|
+
onProgress?.(`Downloading ${owner}/${repo}/skills@${ref}`);
|
|
2006
|
+
try {
|
|
2007
|
+
const { dir } = await downloadTemplate(`github:${owner}/${repo}/skills#${ref}`, {
|
|
2008
|
+
dir: tempDir,
|
|
2009
|
+
force: true,
|
|
2010
|
+
auth: getGitHubToken() || void 0
|
|
2011
|
+
});
|
|
2012
|
+
const skills = [];
|
|
2013
|
+
for (const { dir: skillDir, repoPath } of findSkillDirs(dir, "skills")) {
|
|
2014
|
+
const skill = readLocalSkill(skillDir, repoPath);
|
|
2015
|
+
if (skill) skills.push(skill);
|
|
2016
|
+
}
|
|
2017
|
+
if (skills.length > 0) {
|
|
2018
|
+
onProgress?.(`Found ${skills.length} skill(s)`);
|
|
2019
|
+
return skills;
|
|
2020
|
+
}
|
|
2021
|
+
} catch {}
|
|
2022
|
+
const content = await fetchGitHubRaw(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/SKILL.md`);
|
|
2023
|
+
if (content) {
|
|
2024
|
+
const fm = parseSkillFrontmatterName(content);
|
|
2025
|
+
onProgress?.("Found 1 skill");
|
|
2026
|
+
return [{
|
|
2027
|
+
name: fm.name || repo,
|
|
2028
|
+
description: fm.description || "",
|
|
2029
|
+
path: "",
|
|
2030
|
+
content,
|
|
2031
|
+
files: []
|
|
2032
|
+
}];
|
|
2033
|
+
}
|
|
2034
|
+
return [];
|
|
2035
|
+
} catch {
|
|
2036
|
+
return [];
|
|
2037
|
+
} finally {
|
|
2038
|
+
rmSync(tempDir, {
|
|
2039
|
+
recursive: true,
|
|
2040
|
+
force: true
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
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] : [] };
|
|
2059
|
+
}
|
|
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
|
+
}] };
|
|
2079
|
+
}
|
|
2080
|
+
return { skills: [] };
|
|
2081
|
+
} catch {
|
|
2082
|
+
return { skills: [] };
|
|
2083
|
+
} finally {
|
|
2084
|
+
rmSync(tempDir, {
|
|
2085
|
+
recursive: true,
|
|
2086
|
+
force: true
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
const STATIC_REGEX_1$2 = /^[\^~>=<\d]/;
|
|
2091
|
+
function parseVersionSpecifier(name, version, cwd) {
|
|
2092
|
+
if (version.startsWith("link:")) {
|
|
2093
|
+
const linkedPkg = readPackageJsonSafe(join(resolve(cwd, version.slice(5)), "package.json"));
|
|
2094
|
+
if (linkedPkg) return {
|
|
2095
|
+
name: linkedPkg.parsed.name || name,
|
|
2096
|
+
version: linkedPkg.parsed.version || "0.0.0"
|
|
2097
|
+
};
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2100
|
+
if (version.startsWith("npm:")) {
|
|
2101
|
+
const specifier = version.slice(4);
|
|
2102
|
+
const atIndex = specifier.startsWith("@") ? specifier.indexOf("@", 1) : specifier.indexOf("@");
|
|
2103
|
+
const realName = atIndex > 0 ? specifier.slice(0, atIndex) : specifier;
|
|
2104
|
+
return {
|
|
2105
|
+
name: realName,
|
|
2106
|
+
version: resolveInstalledVersion(realName, cwd) || "*"
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
if (version.startsWith("file:") || version.startsWith("git:") || version.startsWith("git+")) return null;
|
|
2110
|
+
const installed = resolveInstalledVersion(name, cwd);
|
|
2111
|
+
if (installed) return {
|
|
2112
|
+
name,
|
|
2113
|
+
version: installed
|
|
2114
|
+
};
|
|
2115
|
+
if (STATIC_REGEX_1$2.test(version)) return {
|
|
2116
|
+
name,
|
|
2117
|
+
version: version.replace(VERSION_RANGE_PREFIX_RE, "")
|
|
2118
|
+
};
|
|
2119
|
+
if (version.startsWith("catalog:") || version.startsWith("workspace:")) return {
|
|
2120
|
+
name,
|
|
2121
|
+
version: "*"
|
|
2122
|
+
};
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
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"));
|
|
2129
|
+
try {
|
|
2130
|
+
return readPackageJsonSafe(req.resolve(`${name}/package.json`))?.parsed.version || null;
|
|
2131
|
+
} catch {
|
|
2132
|
+
try {
|
|
2133
|
+
let dir = dirname(req.resolve(name));
|
|
2134
|
+
while (dir && basename(dir) !== "node_modules") {
|
|
2135
|
+
const pkg = readPackageJsonSafe(join(dir, "package.json"));
|
|
2136
|
+
if (pkg) return pkg.parsed.version || null;
|
|
2137
|
+
const parent = dirname(dir);
|
|
2138
|
+
if (parent === dir) break;
|
|
2139
|
+
dir = parent;
|
|
2140
|
+
}
|
|
2141
|
+
} catch {}
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
async function readLocalDependencies(cwd) {
|
|
2146
|
+
const result = readPackageJsonSafe(join(cwd, "package.json"));
|
|
2147
|
+
if (!result) throw new Error("No package.json found in current directory");
|
|
2148
|
+
const pkg = result.parsed;
|
|
2149
|
+
const deps = {
|
|
2150
|
+
...pkg.dependencies,
|
|
2151
|
+
...pkg.devDependencies
|
|
2152
|
+
};
|
|
2153
|
+
const results = [];
|
|
2154
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
2155
|
+
const parsed = parseVersionSpecifier(name, version, cwd);
|
|
2156
|
+
if (parsed) results.push(parsed);
|
|
2157
|
+
}
|
|
2158
|
+
return results;
|
|
2159
|
+
}
|
|
2160
|
+
function readLocalPackageInfo(localPath) {
|
|
2161
|
+
const result = readPackageJsonSafe(join(localPath, "package.json"));
|
|
2162
|
+
if (!result) return null;
|
|
2163
|
+
const pkg = result.parsed;
|
|
2164
|
+
let repoUrl;
|
|
2165
|
+
if (pkg.repository?.url) repoUrl = normalizeRepoUrl(pkg.repository.url);
|
|
2166
|
+
else if (typeof pkg.repository === "string") repoUrl = normalizeRepoUrl(pkg.repository);
|
|
2167
|
+
return {
|
|
2168
|
+
name: pkg.name,
|
|
2169
|
+
version: pkg.version || "0.0.0",
|
|
2170
|
+
description: pkg.description,
|
|
2171
|
+
repoUrl,
|
|
2172
|
+
localPath
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
async function resolveLocalPackageDocs(localPath) {
|
|
2176
|
+
const info = readLocalPackageInfo(localPath);
|
|
2177
|
+
if (!info) return null;
|
|
2178
|
+
const result = {
|
|
2179
|
+
name: info.name,
|
|
2180
|
+
version: info.version,
|
|
2181
|
+
description: info.description,
|
|
2182
|
+
repoUrl: info.repoUrl
|
|
2183
|
+
};
|
|
2184
|
+
if (info.repoUrl?.includes("github.com")) {
|
|
2185
|
+
const gh = parseGitHubUrl(info.repoUrl);
|
|
2186
|
+
if (gh) {
|
|
2187
|
+
const gitDocs = await fetchGitDocs(gh.owner, gh.repo, info.version, info.name);
|
|
2188
|
+
if (gitDocs) {
|
|
2189
|
+
result.gitDocsUrl = gitDocs.baseUrl;
|
|
2190
|
+
result.gitRef = gitDocs.ref;
|
|
2191
|
+
result.gitDocsFallback = gitDocs.fallback;
|
|
2192
|
+
}
|
|
2193
|
+
const readmeUrl = await fetchReadme(gh.owner, gh.repo, void 0, result.gitRef);
|
|
2194
|
+
if (readmeUrl) result.readmeUrl = readmeUrl;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (!result.readmeUrl && !result.gitDocsUrl) {
|
|
2198
|
+
const readmeFile = readdirSync(localPath).find((f) => README_FILENAME_RE.test(f));
|
|
2199
|
+
if (readmeFile) result.readmeUrl = pathToFileURL(join(localPath, readmeFile)).href;
|
|
2200
|
+
}
|
|
2201
|
+
if (!result.readmeUrl && !result.gitDocsUrl) return null;
|
|
2202
|
+
return result;
|
|
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
|
+
}
|
|
2242
|
+
async function fetchPkgDist(name, version) {
|
|
2243
|
+
const cacheDir = getCacheDir(name, version);
|
|
2244
|
+
const pkgDir = join(cacheDir, "pkg");
|
|
2245
|
+
if (existsSync(join(pkgDir, "package.json"))) return pkgDir;
|
|
2246
|
+
const data = await $fetch(`https://registry.npmjs.org/${name}/${version}`).catch(() => null);
|
|
2247
|
+
if (!data) return null;
|
|
2248
|
+
const tarballUrl = data.dist?.tarball;
|
|
2249
|
+
if (!tarballUrl) return null;
|
|
2250
|
+
const tarballRes = await fetch(tarballUrl, { headers: { "User-Agent": SKILLD_USER_AGENT } }).catch(() => null);
|
|
2251
|
+
if (!tarballRes?.ok || !tarballRes.body) return null;
|
|
2252
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
2253
|
+
const tmpTarball = join(cacheDir, "_pkg.tgz");
|
|
2254
|
+
const fileStream = createWriteStream(tmpTarball);
|
|
2255
|
+
const fileClosed = new Promise((resolve) => fileStream.once("close", resolve));
|
|
2256
|
+
const reader = tarballRes.body.getReader();
|
|
2257
|
+
try {
|
|
2258
|
+
await new Promise((res, reject) => {
|
|
2259
|
+
const writable = new Writable({ write(chunk, _encoding, callback) {
|
|
2260
|
+
fileStream.write(chunk, callback);
|
|
2261
|
+
} });
|
|
2262
|
+
writable.on("finish", () => {
|
|
2263
|
+
fileStream.end();
|
|
2264
|
+
});
|
|
2265
|
+
fileStream.on("close", () => res());
|
|
2266
|
+
writable.on("error", reject);
|
|
2267
|
+
fileStream.on("error", reject);
|
|
2268
|
+
function pump() {
|
|
2269
|
+
reader.read().then(({ done, value }) => {
|
|
2270
|
+
if (done) {
|
|
2271
|
+
writable.end();
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
writable.write(value, () => pump());
|
|
2275
|
+
}).catch(reject);
|
|
2276
|
+
}
|
|
2277
|
+
pump();
|
|
2278
|
+
});
|
|
2279
|
+
const { status } = spawnSync("tar", [
|
|
2280
|
+
"xzf",
|
|
2281
|
+
tmpTarball,
|
|
2282
|
+
"--strip-components=1",
|
|
2283
|
+
"-C",
|
|
2284
|
+
pkgDir
|
|
2285
|
+
], { stdio: "ignore" });
|
|
2286
|
+
if (status !== 0) {
|
|
2287
|
+
rmSync(pkgDir, {
|
|
2288
|
+
recursive: true,
|
|
2289
|
+
force: true
|
|
2290
|
+
});
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
return pkgDir;
|
|
2294
|
+
} catch {
|
|
2295
|
+
rmSync(pkgDir, {
|
|
2296
|
+
recursive: true,
|
|
2297
|
+
force: true
|
|
2298
|
+
});
|
|
2299
|
+
return null;
|
|
2300
|
+
} finally {
|
|
2301
|
+
reader.cancel().catch(() => {});
|
|
2302
|
+
fileStream.destroy();
|
|
2303
|
+
await fileClosed;
|
|
2304
|
+
try {
|
|
2305
|
+
rmSync(tmpTarball, { force: true });
|
|
2306
|
+
} catch {}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
async function fetchLatestVersion(packageName) {
|
|
2310
|
+
const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
|
|
2311
|
+
if (data?.version) return data.version;
|
|
2312
|
+
return (await $fetch(`https://registry.npmjs.org/${packageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null))?.["dist-tags"]?.latest || null;
|
|
2313
|
+
}
|
|
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
|
+
}
|
|
2
2756
|
function semverValid(v) {
|
|
3
2757
|
return valid(v, true);
|
|
4
2758
|
}
|
|
@@ -8,6 +2762,6 @@ function semverGt(a, b) {
|
|
|
8
2762
|
function semverDiff(a, b) {
|
|
9
2763
|
return diff(a, b);
|
|
10
2764
|
}
|
|
11
|
-
export { semverGt as n, semverValid as r, semverDiff as t };
|
|
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 };
|
|
12
2766
|
|
|
13
2767
|
//# sourceMappingURL=semver.mjs.map
|