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.
Files changed (156) hide show
  1. package/dist/_chunks/add.mjs +66 -0
  2. package/dist/_chunks/add.mjs.map +1 -0
  3. package/dist/_chunks/agent-prompt.mjs +88 -0
  4. package/dist/_chunks/agent-prompt.mjs.map +1 -0
  5. package/dist/_chunks/agent.mjs +81 -57
  6. package/dist/_chunks/agent.mjs.map +1 -1
  7. package/dist/_chunks/args.mjs +42 -0
  8. package/dist/_chunks/args.mjs.map +1 -0
  9. package/dist/_chunks/assemble.mjs +10 -7
  10. package/dist/_chunks/assemble.mjs.map +1 -1
  11. package/dist/_chunks/author.mjs +33 -17
  12. package/dist/_chunks/author.mjs.map +1 -1
  13. package/dist/_chunks/cache.mjs +143 -183
  14. package/dist/_chunks/cache.mjs.map +1 -1
  15. package/dist/_chunks/cache2.mjs +7 -6
  16. package/dist/_chunks/cache2.mjs.map +1 -1
  17. package/dist/_chunks/client.mjs +117 -0
  18. package/dist/_chunks/client.mjs.map +1 -0
  19. package/dist/_chunks/core.mjs +5 -5
  20. package/dist/_chunks/detect.mjs +53 -43
  21. package/dist/_chunks/detect.mjs.map +1 -1
  22. package/dist/_chunks/eject.mjs +69 -0
  23. package/dist/_chunks/eject.mjs.map +1 -0
  24. package/dist/_chunks/embedding-cache2.mjs +1 -1
  25. package/dist/_chunks/env.mjs +19 -0
  26. package/dist/_chunks/env.mjs.map +1 -0
  27. package/dist/_chunks/install-many.mjs +376 -0
  28. package/dist/_chunks/install-many.mjs.map +1 -0
  29. package/dist/_chunks/install.mjs +81 -326
  30. package/dist/_chunks/install.mjs.map +1 -1
  31. package/dist/_chunks/intro.mjs +63 -0
  32. package/dist/_chunks/intro.mjs.map +1 -0
  33. package/dist/_chunks/list.mjs +2 -2
  34. package/dist/_chunks/list.mjs.map +1 -1
  35. package/dist/_chunks/lockfile.mjs +3 -2
  36. package/dist/_chunks/lockfile.mjs.map +1 -1
  37. package/dist/_chunks/login.mjs +233 -0
  38. package/dist/_chunks/login.mjs.map +1 -0
  39. package/dist/_chunks/logout.mjs +27 -0
  40. package/dist/_chunks/logout.mjs.map +1 -0
  41. package/dist/_chunks/map.mjs +11 -0
  42. package/dist/_chunks/map.mjs.map +1 -0
  43. package/dist/_chunks/markdown.mjs +79 -54
  44. package/dist/_chunks/markdown.mjs.map +1 -1
  45. package/dist/_chunks/menu.mjs +33 -0
  46. package/dist/_chunks/menu.mjs.map +1 -0
  47. package/dist/_chunks/model-picker.mjs +61 -0
  48. package/dist/_chunks/model-picker.mjs.map +1 -0
  49. package/dist/_chunks/monorepo.mjs +4 -2
  50. package/dist/_chunks/monorepo.mjs.map +1 -1
  51. package/dist/_chunks/package-json.mjs.map +1 -1
  52. package/dist/_chunks/paths.mjs +3 -5
  53. package/dist/_chunks/paths.mjs.map +1 -1
  54. package/dist/_chunks/{sync-pipeline.mjs → pipeline.mjs} +346 -313
  55. package/dist/_chunks/pipeline.mjs.map +1 -0
  56. package/dist/_chunks/pool2.mjs +1 -1
  57. package/dist/_chunks/portable.mjs +151 -0
  58. package/dist/_chunks/portable.mjs.map +1 -0
  59. package/dist/_chunks/prepare-hook.mjs +2 -0
  60. package/dist/_chunks/prepare-hook2.mjs +61 -0
  61. package/dist/_chunks/prepare-hook2.mjs.map +1 -0
  62. package/dist/_chunks/prepare.mjs +47 -3
  63. package/dist/_chunks/prepare.mjs.map +1 -1
  64. package/dist/_chunks/prepare2.mjs +7 -6
  65. package/dist/_chunks/prepare2.mjs.map +1 -1
  66. package/dist/_chunks/prompts.mjs +484 -74
  67. package/dist/_chunks/prompts.mjs.map +1 -1
  68. package/dist/_chunks/pull.mjs +219 -0
  69. package/dist/_chunks/pull.mjs.map +1 -0
  70. package/dist/_chunks/regex.mjs +19 -0
  71. package/dist/_chunks/regex.mjs.map +1 -0
  72. package/dist/_chunks/retriv.mjs +2 -171
  73. package/dist/_chunks/retriv2.mjs +159 -0
  74. package/dist/_chunks/retriv2.mjs.map +1 -0
  75. package/dist/_chunks/sanitize.mjs +12 -9
  76. package/dist/_chunks/sanitize.mjs.map +1 -1
  77. package/dist/_chunks/search-helpers.mjs +8 -6
  78. package/dist/_chunks/search-helpers.mjs.map +1 -1
  79. package/dist/_chunks/search-interactive.mjs +23 -20
  80. package/dist/_chunks/search-interactive.mjs.map +1 -1
  81. package/dist/_chunks/search.mjs +3 -3
  82. package/dist/_chunks/search.mjs.map +1 -1
  83. package/dist/_chunks/semver.mjs +2755 -1
  84. package/dist/_chunks/semver.mjs.map +1 -1
  85. package/dist/_chunks/skill-installer2.mjs +10 -11
  86. package/dist/_chunks/skill-installer2.mjs.map +1 -1
  87. package/dist/_chunks/skills.mjs +6 -7
  88. package/dist/_chunks/skills.mjs.map +1 -1
  89. package/dist/_chunks/store.mjs +107 -0
  90. package/dist/_chunks/store.mjs.map +1 -0
  91. package/dist/_chunks/sync.mjs +411 -910
  92. package/dist/_chunks/sync.mjs.map +1 -1
  93. package/dist/_chunks/sync2.mjs +2 -5
  94. package/dist/_chunks/telemetry.mjs +26 -0
  95. package/dist/_chunks/telemetry.mjs.map +1 -0
  96. package/dist/_chunks/uninstall.mjs +12 -9
  97. package/dist/_chunks/uninstall.mjs.map +1 -1
  98. package/dist/_chunks/update.mjs +171 -0
  99. package/dist/_chunks/update.mjs.map +1 -0
  100. package/dist/_chunks/upload.mjs +3 -3
  101. package/dist/_chunks/validate.mjs +1 -1
  102. package/dist/_chunks/version.mjs +16 -17
  103. package/dist/_chunks/version.mjs.map +1 -1
  104. package/dist/_chunks/whoami.mjs +21 -0
  105. package/dist/_chunks/whoami.mjs.map +1 -0
  106. package/dist/_chunks/wizard.mjs +2 -190
  107. package/dist/_chunks/wizard2.mjs +200 -0
  108. package/dist/_chunks/wizard2.mjs.map +1 -0
  109. package/dist/cli.mjs +72 -53
  110. package/dist/cli.mjs.map +1 -1
  111. package/dist/prepare.mjs +4 -3
  112. package/dist/prepare.mjs.map +1 -1
  113. package/dist/retriv/worker.d.mts +5 -1
  114. package/dist/retriv/worker.d.mts.map +1 -1
  115. package/dist/retriv/worker.mjs +1 -1
  116. package/package.json +19 -28
  117. package/dist/_chunks/author-group.mjs +0 -17
  118. package/dist/_chunks/author-group.mjs.map +0 -1
  119. package/dist/_chunks/cli-helpers.mjs +0 -335
  120. package/dist/_chunks/cli-helpers.mjs.map +0 -1
  121. package/dist/_chunks/cli-helpers2.mjs +0 -2
  122. package/dist/_chunks/index.d.mts +0 -344
  123. package/dist/_chunks/index.d.mts.map +0 -1
  124. package/dist/_chunks/index2.d.mts +0 -279
  125. package/dist/_chunks/index2.d.mts.map +0 -1
  126. package/dist/_chunks/index3.d.mts +0 -44
  127. package/dist/_chunks/index3.d.mts.map +0 -1
  128. package/dist/_chunks/index4.d.mts +0 -553
  129. package/dist/_chunks/index4.d.mts.map +0 -1
  130. package/dist/_chunks/package-registry.mjs +0 -465
  131. package/dist/_chunks/package-registry.mjs.map +0 -1
  132. package/dist/_chunks/retriv.mjs.map +0 -1
  133. package/dist/_chunks/setup.mjs +0 -17
  134. package/dist/_chunks/setup.mjs.map +0 -1
  135. package/dist/_chunks/sources.mjs +0 -2654
  136. package/dist/_chunks/sources.mjs.map +0 -1
  137. package/dist/_chunks/sync-pipeline.mjs.map +0 -1
  138. package/dist/_chunks/sync-registry.mjs +0 -65
  139. package/dist/_chunks/sync-registry.mjs.map +0 -1
  140. package/dist/_chunks/types.d.mts +0 -76
  141. package/dist/_chunks/types.d.mts.map +0 -1
  142. package/dist/_chunks/types2.d.mts +0 -88
  143. package/dist/_chunks/types2.d.mts.map +0 -1
  144. package/dist/_chunks/wizard.mjs.map +0 -1
  145. package/dist/agent/index.d.mts +0 -2
  146. package/dist/agent/index.mjs +0 -4
  147. package/dist/cache/index.d.mts +0 -2
  148. package/dist/cache/index.mjs +0 -5
  149. package/dist/index.d.mts +0 -6
  150. package/dist/index.mjs +0 -6
  151. package/dist/retriv/index.d.mts +0 -3
  152. package/dist/retriv/index.mjs +0 -2
  153. package/dist/sources/index.d.mts +0 -3
  154. package/dist/sources/index.mjs +0 -3
  155. package/dist/types.d.mts +0 -4
  156. package/dist/types.mjs +0 -1
@@ -1,2654 +0,0 @@
1
- import { t as yamlEscape } from "./yaml.mjs";
2
- import { i as readPackageJsonSafe } from "./package-json.mjs";
3
- import { t as getCacheDir } from "./version.mjs";
4
- import { i as parseFrontmatter, n as extractLinks, r as extractTitle, t as extractDescription } from "./markdown.mjs";
5
- import { n as getCrawlUrl, r as getDocOverride, t as getBlogPreset } from "./package-registry.mjs";
6
- import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { basename, dirname, join, resolve } from "pathe";
9
- import { htmlToMarkdown } from "mdream";
10
- import pLimit from "p-limit";
11
- import { spawnSync } from "node:child_process";
12
- import { ofetch } from "ofetch";
13
- import { fileURLToPath, pathToFileURL } from "node:url";
14
- import { crawlAndGenerate } from "@mdream/crawl";
15
- import { glob } from "tinyglobby";
16
- import { downloadTemplate } from "giget";
17
- import { Writable } from "node:stream";
18
- import { resolvePathSync } from "mlly";
19
- const BOT_USERS = new Set([
20
- "renovate[bot]",
21
- "dependabot[bot]",
22
- "renovate-bot",
23
- "dependabot",
24
- "github-actions[bot]"
25
- ]);
26
- const isoDate = (iso) => iso.split("T")[0];
27
- function buildFrontmatter(fields) {
28
- const lines = ["---"];
29
- for (const [k, v] of Object.entries(fields)) if (v !== void 0) lines.push(`${k}: ${typeof v === "string" ? yamlEscape(v) : v}`);
30
- lines.push("---");
31
- return lines.join("\n");
32
- }
33
- function hasCodeBlock(text) {
34
- return /```[\s\S]*?```/.test(text) || /`[^`]+`/.test(text);
35
- }
36
- const COMMENT_NOISE_RE = /^(?:\+1|👍|same here|any update|bump|following|is there any progress|when will this|me too|i have the same|same issue|thanks|thank you)[\s!?.]*$/i;
37
- function truncateBody(body, limit) {
38
- if (body.length <= limit) return body;
39
- const codeBlockRe = /```[\s\S]*?```/g;
40
- let lastSafeEnd = limit;
41
- let match;
42
- while ((match = codeBlockRe.exec(body)) !== null) {
43
- const blockStart = match.index;
44
- const blockEnd = blockStart + match[0].length;
45
- if (blockStart < limit && blockEnd > limit) {
46
- if (blockEnd <= limit + 500) lastSafeEnd = blockEnd;
47
- else lastSafeEnd = blockStart;
48
- break;
49
- }
50
- }
51
- const slice = body.slice(0, lastSafeEnd);
52
- const lastParagraph = slice.lastIndexOf("\n\n");
53
- if (lastParagraph > lastSafeEnd * .6) return `${slice.slice(0, lastParagraph)}\n\n...`;
54
- return `${slice}...`;
55
- }
56
- let _ghToken;
57
- function getGitHubToken() {
58
- if (_ghToken !== void 0) return _ghToken;
59
- try {
60
- const { stdout } = spawnSync("gh", ["auth", "token"], {
61
- encoding: "utf-8",
62
- timeout: 5e3,
63
- stdio: [
64
- "ignore",
65
- "pipe",
66
- "ignore"
67
- ]
68
- });
69
- _ghToken = stdout?.trim() || null;
70
- } catch {
71
- _ghToken = null;
72
- }
73
- return _ghToken;
74
- }
75
- const _needsAuth = /* @__PURE__ */ new Set();
76
- function markRepoPrivate(owner, repo) {
77
- _needsAuth.add(`${owner}/${repo}`);
78
- }
79
- function isKnownPrivateRepo(owner, repo) {
80
- return _needsAuth.has(`${owner}/${repo}`);
81
- }
82
- async function fetchUnghOrApi(owner, repo, ungh, api) {
83
- if (!isKnownPrivateRepo(owner, repo)) {
84
- const r = await ungh().catch(() => null);
85
- if (r) return r;
86
- }
87
- const r = await api();
88
- if (r) markRepoPrivate(owner, repo);
89
- return r;
90
- }
91
- const GH_API = "https://api.github.com";
92
- const ghApiFetch = ofetch.create({
93
- retry: 2,
94
- retryDelay: 500,
95
- timeout: 15e3,
96
- headers: { "User-Agent": "skilld/1.0" }
97
- });
98
- const LINK_NEXT_RE = /<([^>]+)>;\s*rel="next"/;
99
- function parseLinkNext(header) {
100
- if (!header) return null;
101
- return header.match(LINK_NEXT_RE)?.[1] ?? null;
102
- }
103
- async function ghApi(endpoint) {
104
- const token = getGitHubToken();
105
- if (!token) return null;
106
- return ghApiFetch(`${GH_API}/${endpoint}`, { headers: { Authorization: `token ${token}` } }).catch(() => null);
107
- }
108
- async function ghApiPaginated(endpoint) {
109
- const token = getGitHubToken();
110
- if (!token) return [];
111
- const headers = { Authorization: `token ${token}` };
112
- const results = [];
113
- let url = `${GH_API}/${endpoint}`;
114
- while (url) {
115
- const res = await ghApiFetch.raw(url, { headers }).catch(() => null);
116
- if (!res?.ok || !Array.isArray(res._data)) break;
117
- results.push(...res._data);
118
- url = parseLinkNext(res.headers.get("link"));
119
- }
120
- return results;
121
- }
122
- const SKILLD_USER_AGENT = "skilld/1.0 (+https://github.com/harlan-zw/skilld)";
123
- const $fetch = ofetch.create({
124
- retry: 3,
125
- retryDelay: 1e3,
126
- retryStatusCodes: [
127
- 408,
128
- 429,
129
- 500,
130
- 502,
131
- 503,
132
- 504
133
- ],
134
- timeout: 15e3,
135
- headers: { "User-Agent": SKILLD_USER_AGENT }
136
- });
137
- function createRateLimitedRunner(intervalMs) {
138
- let queue = Promise.resolve();
139
- let lastRunAt = 0;
140
- return async function runRateLimited(task) {
141
- const run = async () => {
142
- const waitMs = intervalMs - (Date.now() - lastRunAt);
143
- if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
144
- lastRunAt = Date.now();
145
- return task();
146
- };
147
- const request = queue.then(run, run);
148
- queue = request.then(() => void 0, () => void 0);
149
- return request;
150
- };
151
- }
152
- async function fetchText(url) {
153
- return $fetch(url, { responseType: "text" }).catch(() => null);
154
- }
155
- const RAW_GH_RE = /raw\.githubusercontent\.com\/([^/]+)\/([^/]+)/;
156
- function extractGitHubRepo(url) {
157
- const match = url.match(RAW_GH_RE);
158
- return match ? {
159
- owner: match[1],
160
- repo: match[2]
161
- } : null;
162
- }
163
- async function fetchGitHubRaw(url) {
164
- const gh = extractGitHubRepo(url);
165
- if (!(gh ? isKnownPrivateRepo(gh.owner, gh.repo) : false)) {
166
- const content = await fetchText(url);
167
- if (content) return content;
168
- }
169
- if (!gh) return null;
170
- const token = getGitHubToken();
171
- if (!token) return null;
172
- const content = await $fetch(url, {
173
- responseType: "text",
174
- headers: { Authorization: `token ${token}` }
175
- }).catch(() => null);
176
- if (content) markRepoPrivate(gh.owner, gh.repo);
177
- return content;
178
- }
179
- async function verifyUrl(url) {
180
- const res = await $fetch.raw(url, { method: "HEAD" }).catch(() => null);
181
- if (!res) return false;
182
- return !(res.headers.get("content-type") || "").includes("text/html");
183
- }
184
- const USELESS_HOSTS = new Set([
185
- "twitter.com",
186
- "x.com",
187
- "facebook.com",
188
- "linkedin.com",
189
- "youtube.com",
190
- "instagram.com",
191
- "npmjs.com",
192
- "www.npmjs.com",
193
- "yarnpkg.com"
194
- ]);
195
- function isUselessDocsUrl(url) {
196
- try {
197
- const { hostname } = new URL(url);
198
- return USELESS_HOSTS.has(hostname);
199
- } catch {
200
- return false;
201
- }
202
- }
203
- function isGitHubRepoUrl(url) {
204
- try {
205
- const parsed = new URL(url);
206
- return parsed.hostname === "github.com" || parsed.hostname === "www.github.com";
207
- } catch {
208
- return false;
209
- }
210
- }
211
- function isLikelyCodeHostUrl(url) {
212
- if (!url) return false;
213
- try {
214
- const parsed = new URL(url);
215
- return [
216
- "github.com",
217
- "www.github.com",
218
- "gitlab.com",
219
- "www.gitlab.com"
220
- ].includes(parsed.hostname);
221
- } catch {
222
- return false;
223
- }
224
- }
225
- function parseGitHubUrl(url) {
226
- const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[/#]|$)/);
227
- if (!match) return null;
228
- return {
229
- owner: match[1],
230
- repo: match[2]
231
- };
232
- }
233
- function parseGitHubRepoSlug(url) {
234
- if (!url) return void 0;
235
- const parsed = parseGitHubUrl(url);
236
- return parsed ? `${parsed.owner}/${parsed.repo}` : void 0;
237
- }
238
- function normalizeRepoUrl(url) {
239
- return url.replace(/^git\+/, "").replace(/#.*$/, "").replace(/\.git$/, "").replace(/^git:\/\//, "https://").replace(/^ssh:\/\/git@github\.com/, "https://github.com").replace(/^git@github\.com:/, "https://github.com/");
240
- }
241
- function parsePackageSpec(spec) {
242
- if (spec.startsWith("@")) {
243
- const slashIdx = spec.indexOf("/");
244
- if (slashIdx !== -1) {
245
- const atIdx = spec.indexOf("@", slashIdx + 1);
246
- if (atIdx !== -1) return {
247
- name: spec.slice(0, atIdx),
248
- tag: spec.slice(atIdx + 1)
249
- };
250
- }
251
- return { name: spec };
252
- }
253
- const atIdx = spec.indexOf("@");
254
- if (atIdx !== -1) return {
255
- name: spec.slice(0, atIdx),
256
- tag: spec.slice(atIdx + 1)
257
- };
258
- return { name: spec };
259
- }
260
- function extractBranchHint(url) {
261
- const hash = url.indexOf("#");
262
- if (hash === -1) return void 0;
263
- const fragment = url.slice(hash + 1);
264
- if (!fragment || fragment === "readme") return void 0;
265
- return fragment;
266
- }
267
- function parseSemver(version) {
268
- const clean = version.replace(/^v/, "");
269
- const match = clean.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
270
- if (!match) return null;
271
- return {
272
- major: +match[1],
273
- minor: match[2] ? +match[2] : 0,
274
- patch: match[3] ? +match[3] : 0,
275
- raw: clean
276
- };
277
- }
278
- function extractVersion(tag, packageName) {
279
- if (packageName) {
280
- const atMatch = tag.match(new RegExp(`^${escapeRegex(packageName)}@(.+)$`));
281
- if (atMatch) return atMatch[1];
282
- const dashMatch = tag.match(new RegExp(`^${escapeRegex(packageName)}-v?(.+)$`));
283
- if (dashMatch) return dashMatch[1];
284
- }
285
- return tag.replace(/^v/, "");
286
- }
287
- function escapeRegex(str) {
288
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
289
- }
290
- function tagMatchesPackage(tag, packageName) {
291
- return tag.startsWith(`${packageName}@`) || tag.startsWith(`${packageName}-v`) || tag.startsWith(`${packageName}-`);
292
- }
293
- function isPrerelease(version) {
294
- return /^\d+\.\d+\.\d+-.+/.test(version.replace(/^v/, ""));
295
- }
296
- function compareSemver(a, b) {
297
- if (a.major !== b.major) return a.major - b.major;
298
- if (a.minor !== b.minor) return a.minor - b.minor;
299
- return a.patch - b.patch;
300
- }
301
- function mapApiRelease(r) {
302
- return {
303
- id: r.id,
304
- tag: r.tag_name,
305
- name: r.name,
306
- prerelease: r.prerelease,
307
- createdAt: r.created_at,
308
- publishedAt: r.published_at,
309
- markdown: r.body
310
- };
311
- }
312
- async function fetchAllReleases(owner, repo) {
313
- const apiReleases = await ghApiPaginated(`repos/${owner}/${repo}/releases`);
314
- if (apiReleases.length > 0) return apiReleases.map(mapApiRelease);
315
- return (await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`, { signal: AbortSignal.timeout(15e3) }).catch(() => null))?.releases ?? [];
316
- }
317
- function selectReleases(releases, packageName, installedVersion, fromDate) {
318
- const hasMonorepoTags = packageName && releases.some((r) => tagMatchesPackage(r.tag, packageName));
319
- const installedSv = installedVersion ? parseSemver(installedVersion) : null;
320
- const installedIsPrerelease = installedVersion ? isPrerelease(installedVersion) : false;
321
- const fromTs = fromDate ? new Date(fromDate).getTime() : null;
322
- const sorted = releases.filter((r) => {
323
- const ver = extractVersion(r.tag, hasMonorepoTags ? packageName : void 0);
324
- if (!ver) return false;
325
- const sv = parseSemver(ver);
326
- if (!sv) return false;
327
- if (hasMonorepoTags && packageName && !tagMatchesPackage(r.tag, packageName)) return false;
328
- if (fromTs) {
329
- const pubDate = r.publishedAt || r.createdAt;
330
- if (pubDate && new Date(pubDate).getTime() < fromTs) return false;
331
- }
332
- if (r.prerelease) {
333
- if (!installedIsPrerelease || !installedSv) return false;
334
- return sv.major === installedSv.major && sv.minor === installedSv.minor;
335
- }
336
- if (installedSv && compareSemver(sv, installedSv) > 0) return false;
337
- return true;
338
- }).sort((a, b) => {
339
- const verA = extractVersion(a.tag, hasMonorepoTags ? packageName : void 0);
340
- const verB = extractVersion(b.tag, hasMonorepoTags ? packageName : void 0);
341
- if (!verA || !verB) return 0;
342
- return compareSemver(parseSemver(verB), parseSemver(verA));
343
- });
344
- return fromDate ? sorted : sorted.slice(0, 20);
345
- }
346
- function formatRelease(release, packageName) {
347
- const date = isoDate(release.publishedAt || release.createdAt);
348
- const version = extractVersion(release.tag, packageName) || release.tag;
349
- const fm = [
350
- "---",
351
- `tag: ${yamlEscape(release.tag)}`,
352
- `version: ${yamlEscape(version)}`,
353
- `published: ${date}`
354
- ];
355
- if (release.name && release.name !== release.tag) fm.push(`name: ${yamlEscape(release.name)}`);
356
- fm.push("---");
357
- return `${fm.join("\n")}\n\n# ${release.name || release.tag}\n\n${release.markdown}`;
358
- }
359
- function generateReleaseIndex(releasesOrOpts, packageName) {
360
- const opts = Array.isArray(releasesOrOpts) ? {
361
- releases: releasesOrOpts,
362
- packageName
363
- } : releasesOrOpts;
364
- const { releases, blogReleases, hasChangelog } = opts;
365
- const pkg = opts.packageName;
366
- const lines = [
367
- [
368
- "---",
369
- `total: ${releases.length + (blogReleases?.length ?? 0)}`,
370
- `latest: ${yamlEscape(releases[0]?.tag || "unknown")}`,
371
- "---"
372
- ].join("\n"),
373
- "",
374
- "# Releases Index",
375
- ""
376
- ];
377
- if (blogReleases && blogReleases.length > 0) {
378
- lines.push("## Blog Releases", "");
379
- for (const b of blogReleases) lines.push(`- [${b.version}](./blog-${b.version}.md): ${b.title} (${b.date})`);
380
- lines.push("");
381
- }
382
- if (releases.length > 0) {
383
- if (blogReleases && blogReleases.length > 0) lines.push("## Release Notes", "");
384
- for (const r of releases) {
385
- const date = isoDate(r.publishedAt || r.createdAt);
386
- const filename = r.tag.includes("@") || r.tag.startsWith("v") ? r.tag : `v${r.tag}`;
387
- const sv = parseSemver(extractVersion(r.tag, pkg) || r.tag);
388
- const label = sv?.patch === 0 && sv.minor === 0 ? " **[MAJOR]**" : sv?.patch === 0 ? " **[MINOR]**" : "";
389
- lines.push(`- [${r.tag}](./${filename}.md): ${r.name || r.tag} (${date})${label}`);
390
- }
391
- lines.push("");
392
- }
393
- if (hasChangelog) {
394
- lines.push("## Changelog", "");
395
- lines.push("- [CHANGELOG.md](./CHANGELOG.md)");
396
- lines.push("");
397
- }
398
- return lines.join("\n");
399
- }
400
- function isStubRelease(release) {
401
- const body = (release.markdown || "").trim();
402
- return body.length < 500 && /changelog\.md/i.test(body);
403
- }
404
- async function fetchChangelog(owner, repo, ref, packageName) {
405
- const paths = [];
406
- if (packageName) {
407
- const shortName = packageName.replace(/^@.*\//, "");
408
- const scopeless = packageName.replace(/^@/, "").replace("/", "-");
409
- const candidates = [...new Set([shortName, scopeless])];
410
- for (const name of candidates) paths.push(`packages/${name}/CHANGELOG.md`);
411
- }
412
- paths.push("CHANGELOG.md", "changelog.md", "CHANGES.md");
413
- for (const path of paths) {
414
- const content = await fetchGitHubRaw(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`);
415
- if (content) return content;
416
- }
417
- return null;
418
- }
419
- async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageName, fromDate, changelogRef) {
420
- const selected = selectReleases(await fetchAllReleases(owner, repo), packageName, installedVersion, fromDate);
421
- if (selected.length > 0) {
422
- const docs = selected.filter((r) => !isStubRelease(r)).map((r) => {
423
- return {
424
- path: `releases/${r.tag.includes("@") || r.tag.startsWith("v") ? r.tag : `v${r.tag}`}.md`,
425
- content: formatRelease(r, packageName)
426
- };
427
- });
428
- const changelog = await fetchChangelog(owner, repo, changelogRef || gitRef || selected[0].tag, packageName);
429
- if (changelog && changelog.length < 5e5) docs.push({
430
- path: "releases/CHANGELOG.md",
431
- content: changelog
432
- });
433
- return docs;
434
- }
435
- const changelog = await fetchChangelog(owner, repo, changelogRef || gitRef || "main", packageName);
436
- if (!changelog) return [];
437
- return [{
438
- path: "releases/CHANGELOG.md",
439
- content: changelog
440
- }];
441
- }
442
- function formatBlogRelease(release) {
443
- return `${[
444
- "---",
445
- `version: ${yamlEscape(release.version)}`,
446
- `title: ${yamlEscape(release.title)}`,
447
- `date: ${release.date}`,
448
- `url: ${yamlEscape(release.url)}`,
449
- `source: blog-release`,
450
- "---"
451
- ].join("\n")}\n\n# ${release.title}\n\n${release.markdown}`;
452
- }
453
- async function fetchBlogPost(entry) {
454
- try {
455
- const html = await $fetch(entry.url, {
456
- responseType: "text",
457
- signal: AbortSignal.timeout(1e4)
458
- }).catch(() => null);
459
- if (!html) return null;
460
- let title = "";
461
- const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
462
- if (titleMatch) title = titleMatch[1].trim();
463
- if (!title) {
464
- const metaTitleMatch = html.match(/<title>([^<]+)<\/title>/);
465
- if (metaTitleMatch) title = metaTitleMatch[1].trim();
466
- }
467
- const markdown = htmlToMarkdown(html);
468
- if (!markdown) return null;
469
- return {
470
- version: entry.version,
471
- title: title || entry.title || `Release ${entry.version}`,
472
- date: entry.date,
473
- markdown,
474
- url: entry.url
475
- };
476
- } catch {
477
- return null;
478
- }
479
- }
480
- function filterBlogsByVersion(entries, installedVersion) {
481
- const installedSv = parseSemver(installedVersion);
482
- if (!installedSv) return entries;
483
- return entries.filter((entry) => {
484
- const entrySv = parseSemver(entry.version);
485
- if (!entrySv) return false;
486
- return compareSemver(entrySv, installedSv) <= 0;
487
- });
488
- }
489
- async function fetchBlogReleases(packageName, installedVersion) {
490
- const preset = getBlogPreset(packageName);
491
- if (!preset) return [];
492
- const filteredReleases = filterBlogsByVersion(preset.releases, installedVersion);
493
- if (filteredReleases.length === 0) return [];
494
- const limit = pLimit(3);
495
- const releases = (await Promise.all(filteredReleases.map((entry) => limit(() => fetchBlogPost(entry))))).filter((r) => r !== null);
496
- if (releases.length === 0) return [];
497
- releases.sort((a, b) => {
498
- const aVer = a.version.split(".").map(Number);
499
- const bVer = b.version.split(".").map(Number);
500
- for (let i = 0; i < Math.max(aVer.length, bVer.length); i++) {
501
- const diff = (bVer[i] ?? 0) - (aVer[i] ?? 0);
502
- if (diff !== 0) return diff;
503
- }
504
- return 0;
505
- });
506
- return releases.map((r) => ({
507
- path: `releases/blog-${r.version}.md`,
508
- content: formatBlogRelease(r)
509
- }));
510
- }
511
- function mapInsert(map, key, create) {
512
- let val = map.get(key);
513
- if (val === void 0) {
514
- val = create();
515
- map.set(key, val);
516
- }
517
- return val;
518
- }
519
- async function listFilesAtRef(owner, repo, ref) {
520
- return await fetchUnghOrApi(owner, repo, async () => {
521
- const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/files/${ref}`);
522
- return data.files?.length ? data.files.map((f) => f.path) : null;
523
- }, async () => {
524
- const tree = await ghApi(`repos/${owner}/${repo}/git/trees/${ref}?recursive=1`);
525
- return tree?.tree?.length ? tree.tree.map((f) => f.path) : null;
526
- }) ?? [];
527
- }
528
- async function findGitTag(owner, repo, version, packageName, branchHint) {
529
- const candidates = [`v${version}`, version];
530
- if (packageName) candidates.push(`${packageName}@${version}`);
531
- for (const tag of candidates) {
532
- const files = await listFilesAtRef(owner, repo, tag);
533
- if (files.length > 0) return {
534
- ref: tag,
535
- files
536
- };
537
- }
538
- if (packageName) {
539
- const latestTag = await findLatestReleaseTag(owner, repo, packageName);
540
- if (latestTag) {
541
- const files = await listFilesAtRef(owner, repo, latestTag);
542
- if (files.length > 0) return {
543
- ref: latestTag,
544
- files
545
- };
546
- }
547
- }
548
- const branches = branchHint ? [branchHint, ...["main", "master"].filter((b) => b !== branchHint)] : ["main", "master"];
549
- for (const branch of branches) {
550
- const files = await listFilesAtRef(owner, repo, branch);
551
- if (files.length > 0) return {
552
- ref: branch,
553
- files,
554
- fallback: true
555
- };
556
- }
557
- return null;
558
- }
559
- async function fetchUnghReleases(owner, repo) {
560
- return await fetchUnghOrApi(owner, repo, async () => {
561
- const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`);
562
- return data.releases?.length ? data.releases : null;
563
- }, async () => {
564
- const raw = await ghApiPaginated(`repos/${owner}/${repo}/releases`);
565
- return raw.length > 0 ? raw.map((r) => ({
566
- tag: r.tag_name,
567
- publishedAt: r.published_at
568
- })) : null;
569
- }) ?? [];
570
- }
571
- async function findLatestReleaseTag(owner, repo, packageName) {
572
- const prefix = `${packageName}@`;
573
- return (await fetchUnghReleases(owner, repo)).find((r) => r.tag.startsWith(prefix))?.tag ?? null;
574
- }
575
- const MIN_GIT_DOCS = 5;
576
- const isShallowGitDocs = (n) => n > 0 && n < 5;
577
- function filterDocFiles(files, pathPrefix) {
578
- return files.filter((f) => f.startsWith(pathPrefix) && /\.(?:md|mdx)$/.test(f));
579
- }
580
- const FRAMEWORK_NAMES = new Set([
581
- "vue",
582
- "react",
583
- "solid",
584
- "angular",
585
- "svelte",
586
- "preact",
587
- "lit",
588
- "qwik"
589
- ]);
590
- function filterFrameworkDocs(files, packageName) {
591
- if (!packageName) return files;
592
- const shortName = packageName.replace(/^@.*\//, "");
593
- const targetFramework = [...FRAMEWORK_NAMES].find((fw) => shortName.includes(fw));
594
- if (!targetFramework) return files;
595
- const otherFrameworks = [...FRAMEWORK_NAMES].filter((fw) => fw !== targetFramework);
596
- const excludePattern = new RegExp(`\\b(?:${otherFrameworks.join("|")})\\b`);
597
- return files.filter((f) => !excludePattern.test(f));
598
- }
599
- const NOISE_PATTERNS = [
600
- /^\.changeset\//,
601
- /CHANGELOG\.md$/i,
602
- /CONTRIBUTING\.md$/i,
603
- /^\.github\//
604
- ];
605
- const EXCLUDE_DIRS = new Set([
606
- "test",
607
- "tests",
608
- "__tests__",
609
- "fixtures",
610
- "fixture",
611
- "examples",
612
- "example",
613
- "node_modules",
614
- ".git",
615
- "dist",
616
- "build",
617
- "coverage",
618
- "e2e",
619
- "spec",
620
- "mocks",
621
- "__mocks__"
622
- ]);
623
- const DOC_DIR_BONUS = new Set([
624
- "docs",
625
- "documentation",
626
- "pages",
627
- "content",
628
- "website",
629
- "guide",
630
- "guides",
631
- "wiki",
632
- "manual",
633
- "api"
634
- ]);
635
- function hasExcludedDir(path) {
636
- return path.split("/").some((p) => EXCLUDE_DIRS.has(p.toLowerCase()));
637
- }
638
- function getPathDepth(path) {
639
- return path.split("/").filter(Boolean).length;
640
- }
641
- function hasDocDirBonus(path) {
642
- return path.split("/").some((p) => DOC_DIR_BONUS.has(p.toLowerCase()));
643
- }
644
- function scoreDocDir(dir, fileCount) {
645
- const depth = getPathDepth(dir) || 1;
646
- return fileCount * (hasDocDirBonus(dir) ? 1.5 : 1) / depth;
647
- }
648
- function discoverDocFiles(allFiles, packageName) {
649
- const mdFiles = allFiles.filter((f) => /\.(?:md|mdx)$/.test(f)).filter((f) => !NOISE_PATTERNS.some((p) => p.test(f))).filter((f) => f.includes("/"));
650
- if (packageName?.includes("/")) {
651
- const subPkgPrefix = `packages/${packageName.split("/").pop().toLowerCase()}/`;
652
- const subPkgFiles = mdFiles.filter((f) => f.startsWith(subPkgPrefix));
653
- if (subPkgFiles.length >= 3) return {
654
- files: subPkgFiles,
655
- prefix: subPkgPrefix
656
- };
657
- }
658
- const docsGroups = /* @__PURE__ */ new Map();
659
- for (const file of mdFiles) {
660
- const docsIdx = file.lastIndexOf("/docs/");
661
- if (docsIdx === -1) continue;
662
- mapInsert(docsGroups, file.slice(0, docsIdx + 6), () => []).push(file);
663
- }
664
- if (docsGroups.size > 0) {
665
- const largest = [...docsGroups.entries()].sort((a, b) => b[1].length - a[1].length)[0];
666
- if (largest[1].length >= 3) {
667
- const fullPrefix = largest[0];
668
- const docsIdx = fullPrefix.lastIndexOf("docs/");
669
- const stripPrefix = docsIdx > 0 ? fullPrefix.slice(0, docsIdx) : "";
670
- return {
671
- files: largest[1],
672
- prefix: stripPrefix
673
- };
674
- }
675
- }
676
- const dirGroups = /* @__PURE__ */ new Map();
677
- for (const file of mdFiles) {
678
- if (hasExcludedDir(file)) continue;
679
- const lastSlash = file.lastIndexOf("/");
680
- if (lastSlash === -1) continue;
681
- mapInsert(dirGroups, file.slice(0, lastSlash + 1), () => []).push(file);
682
- }
683
- if (dirGroups.size === 0) return null;
684
- const scored = Array.from(dirGroups.entries(), ([dir, files]) => ({
685
- dir,
686
- files,
687
- score: scoreDocDir(dir, files.length)
688
- })).filter((d) => d.files.length >= 5).sort((a, b) => b.score - a.score);
689
- if (scored.length === 0) return null;
690
- const best = scored[0];
691
- return {
692
- files: best.files,
693
- prefix: best.dir
694
- };
695
- }
696
- async function listDocsAtRef(owner, repo, ref, pathPrefix = "docs/") {
697
- return filterDocFiles(await listFilesAtRef(owner, repo, ref), pathPrefix);
698
- }
699
- async function fetchGitDocs(owner, repo, version, packageName, repoUrl) {
700
- const override = packageName ? getDocOverride(packageName) : void 0;
701
- if (override) {
702
- const ref = override.ref || "main";
703
- const fallback = !override.ref;
704
- const files = await listDocsAtRef(override.owner, override.repo, ref, `${override.path}/`);
705
- if (files.length === 0) return null;
706
- return {
707
- baseUrl: `https://raw.githubusercontent.com/${override.owner}/${override.repo}/${ref}`,
708
- ref,
709
- files,
710
- fallback,
711
- docsPrefix: `${override.path}/` !== "docs/" ? `${override.path}/` : void 0
712
- };
713
- }
714
- const tag = await findGitTag(owner, repo, version, packageName, repoUrl ? extractBranchHint(repoUrl) : void 0);
715
- if (!tag) return null;
716
- let docs = filterDocFiles(tag.files, "docs/");
717
- let docsPrefix;
718
- let allFiles;
719
- if (docs.length === 0) {
720
- const discovered = discoverDocFiles(tag.files, packageName);
721
- if (discovered) {
722
- docs = discovered.files;
723
- docsPrefix = discovered.prefix || void 0;
724
- allFiles = tag.files;
725
- }
726
- }
727
- docs = filterFrameworkDocs(docs, packageName);
728
- if (docs.length === 0) return null;
729
- return {
730
- baseUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${tag.ref}`,
731
- ref: tag.ref,
732
- files: docs,
733
- docsPrefix,
734
- allFiles,
735
- fallback: tag.fallback
736
- };
737
- }
738
- function normalizePath(p) {
739
- return p.replace(/^\//, "").replace(/\.(?:md|mdx)$/, "");
740
- }
741
- function validateGitDocsWithLlms(llmsLinks, repoFiles) {
742
- if (llmsLinks.length === 0) return {
743
- isValid: true,
744
- matchRatio: 1
745
- };
746
- const sample = llmsLinks.slice(0, 10);
747
- const normalizedLinks = sample.map((link) => {
748
- let path = link.url;
749
- if (path.startsWith("http")) try {
750
- path = new URL(path).pathname;
751
- } catch {}
752
- return normalizePath(path);
753
- });
754
- const repoNormalized = new Set(repoFiles.map(normalizePath));
755
- let matches = 0;
756
- for (const linkPath of normalizedLinks) for (const repoPath of repoNormalized) if (repoPath === linkPath || repoPath.endsWith(`/${linkPath}`)) {
757
- matches++;
758
- break;
759
- }
760
- const matchRatio = matches / sample.length;
761
- return {
762
- isValid: matchRatio >= .3,
763
- matchRatio
764
- };
765
- }
766
- let _ghAvailable;
767
- function isGhAvailable() {
768
- if (_ghAvailable !== void 0) return _ghAvailable;
769
- const { status } = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
770
- return _ghAvailable = status === 0;
771
- }
772
- const NOISE_LABELS = new Set([
773
- "duplicate",
774
- "stale",
775
- "invalid",
776
- "wontfix",
777
- "won't fix",
778
- "spam",
779
- "off-topic",
780
- "needs triage",
781
- "triage"
782
- ]);
783
- const FEATURE_LABELS = new Set([
784
- "enhancement",
785
- "feature",
786
- "feature request",
787
- "feature-request",
788
- "proposal",
789
- "rfc",
790
- "idea",
791
- "suggestion"
792
- ]);
793
- const BUG_LABELS = new Set([
794
- "bug",
795
- "defect",
796
- "regression",
797
- "error",
798
- "crash",
799
- "fix",
800
- "confirmed",
801
- "verified"
802
- ]);
803
- const QUESTION_LABELS = new Set([
804
- "question",
805
- "help wanted",
806
- "support",
807
- "usage",
808
- "how-to",
809
- "help",
810
- "assistance"
811
- ]);
812
- const DOCS_LABELS = new Set([
813
- "documentation",
814
- "docs",
815
- "doc",
816
- "typo"
817
- ]);
818
- const labelRegexCache = /* @__PURE__ */ new WeakMap();
819
- function getLabelRegex(keywords) {
820
- let re = labelRegexCache.get(keywords);
821
- if (!re) {
822
- const escaped = Array.from(keywords, (k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
823
- re = new RegExp(`\\b(?:${escaped.join("|")})\\b`);
824
- labelRegexCache.set(keywords, re);
825
- }
826
- return re;
827
- }
828
- function labelMatchesAny(label, keywords) {
829
- if (keywords.has(label)) return true;
830
- return getLabelRegex(keywords).test(label);
831
- }
832
- function classifyIssue(labels) {
833
- const lower = labels.map((l) => l.toLowerCase());
834
- if (lower.some((l) => labelMatchesAny(l, BUG_LABELS))) return "bug";
835
- if (lower.some((l) => labelMatchesAny(l, QUESTION_LABELS))) return "question";
836
- if (lower.some((l) => labelMatchesAny(l, DOCS_LABELS))) return "docs";
837
- if (lower.some((l) => labelMatchesAny(l, FEATURE_LABELS))) return "feature";
838
- return "other";
839
- }
840
- function isNoiseIssue(issue) {
841
- if (issue.labels.map((l) => l.toLowerCase()).some((l) => labelMatchesAny(l, NOISE_LABELS))) return true;
842
- if (issue.title.startsWith("☂️") || issue.title.startsWith("[META]") || issue.title.startsWith("[Tracking]")) return true;
843
- return false;
844
- }
845
- function isNonTechnical(issue) {
846
- const body = (issue.body || "").trim();
847
- if (body.length < 200 && !hasCodeBlock(body) && issue.reactions > 50) return true;
848
- if (/\b(?:love|thank|awesome|great work)\b/i.test(issue.title) && !hasCodeBlock(body)) return true;
849
- return false;
850
- }
851
- function freshnessScore(reactions, createdAt) {
852
- return reactions * (1 / (1 + (Date.now() - new Date(createdAt).getTime()) / (365.25 * 24 * 60 * 60 * 1e3) * .6));
853
- }
854
- function applyTypeQuotas(issues, limit) {
855
- const byType = /* @__PURE__ */ new Map();
856
- for (const issue of issues) mapInsert(byType, issue.type, () => []).push(issue);
857
- for (const group of byType.values()) group.sort((a, b) => b.score - a.score);
858
- const quotas = [
859
- ["bug", Math.ceil(limit * .4)],
860
- ["question", Math.ceil(limit * .3)],
861
- ["docs", Math.ceil(limit * .15)],
862
- ["feature", Math.ceil(limit * .1)],
863
- ["other", Math.ceil(limit * .05)]
864
- ];
865
- const selected = [];
866
- const used = /* @__PURE__ */ new Set();
867
- let remaining = limit;
868
- for (const [type, quota] of quotas) {
869
- const group = byType.get(type) || [];
870
- const take = Math.min(quota, group.length, remaining);
871
- for (let i = 0; i < take; i++) {
872
- selected.push(group[i]);
873
- used.add(group[i].number);
874
- remaining--;
875
- }
876
- }
877
- if (remaining > 0) {
878
- const unused = issues.filter((i) => !used.has(i.number) && i.type !== "feature").sort((a, b) => b.score - a.score);
879
- for (const issue of unused) {
880
- if (remaining <= 0) break;
881
- selected.push(issue);
882
- remaining--;
883
- }
884
- }
885
- return selected.sort((a, b) => b.score - a.score);
886
- }
887
- function bodyLimit(reactions) {
888
- if (reactions >= 10) return 2e3;
889
- if (reactions >= 5) return 1500;
890
- return 800;
891
- }
892
- function fetchIssuesByState(owner, repo, state, count, releasedAt, fromDate) {
893
- const fetchCount = Math.min(count * 3, 100);
894
- let datePart = "";
895
- if (fromDate) datePart = state === "closed" ? `+closed:>=${fromDate}` : `+created:>=${fromDate}`;
896
- else if (state === "closed") if (releasedAt) {
897
- datePart = `+closed:>=${isoDate(releasedAt)}`;
898
- const cap = new Date(releasedAt);
899
- cap.setMonth(cap.getMonth() + 6);
900
- if (cap < /* @__PURE__ */ new Date()) datePart += `+closed:<=${isoDate(cap.toISOString())}`;
901
- } else datePart = `+closed:>${oneYearAgo()}`;
902
- else if (releasedAt) {
903
- const date = new Date(releasedAt);
904
- date.setMonth(date.getMonth() + 6);
905
- datePart = `+created:<=${isoDate(date.toISOString())}`;
906
- }
907
- const { stdout: result } = spawnSync("gh", [
908
- "api",
909
- `search/issues?q=${`repo:${owner}/${repo}+is:issue+is:${state}${datePart}`}&sort=reactions&order=desc&per_page=${fetchCount}`,
910
- "-q",
911
- ".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}"
912
- ], {
913
- encoding: "utf-8",
914
- maxBuffer: 10 * 1024 * 1024
915
- });
916
- if (!result) return [];
917
- 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 }) => {
918
- const isMaintainer = [
919
- "OWNER",
920
- "MEMBER",
921
- "COLLABORATOR"
922
- ].includes(authorAssociation);
923
- const isRoadmap = /\broadmap\b/i.test(issue.title) || issue.labels.some((l) => /roadmap/i.test(l));
924
- return {
925
- ...issue,
926
- type: classifyIssue(issue.labels),
927
- topComments: [],
928
- score: freshnessScore(issue.reactions, issue.createdAt) * (isMaintainer && isRoadmap ? 5 : 1)
929
- };
930
- }).sort((a, b) => b.score - a.score).slice(0, count);
931
- }
932
- function oneYearAgo() {
933
- const d = /* @__PURE__ */ new Date();
934
- d.setFullYear(d.getFullYear() - 1);
935
- return isoDate(d.toISOString());
936
- }
937
- function enrichWithComments(owner, repo, issues, topN = 15) {
938
- 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);
939
- if (worth.length === 0) return;
940
- 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(" ")} } }`;
941
- try {
942
- const { stdout: result } = spawnSync("gh", [
943
- "api",
944
- "graphql",
945
- "-f",
946
- `query=${query}`,
947
- "-f",
948
- `owner=${owner}`,
949
- "-f",
950
- `repo=${repo}`
951
- ], {
952
- encoding: "utf-8",
953
- maxBuffer: 10 * 1024 * 1024
954
- });
955
- if (!result) return;
956
- const repo_ = JSON.parse(result)?.data?.repository;
957
- if (!repo_) return;
958
- for (let i = 0; i < worth.length; i++) {
959
- const nodes = repo_[`i${i}`]?.comments?.nodes;
960
- if (!Array.isArray(nodes)) continue;
961
- const issue = worth[i];
962
- const comments = nodes.filter((c) => c.author && !BOT_USERS.has(c.author.login)).filter((c) => !COMMENT_NOISE_RE.test((c.body || "").trim())).map((c) => {
963
- const isMaintainer = [
964
- "OWNER",
965
- "MEMBER",
966
- "COLLABORATOR"
967
- ].includes(c.authorAssociation);
968
- const body = c.body || "";
969
- const reactions = c.reactions?.totalCount || 0;
970
- const _score = (isMaintainer ? 3 : 1) * (hasCodeBlock(body) ? 2 : 1) * (1 + reactions);
971
- return {
972
- body,
973
- author: c.author.login,
974
- reactions,
975
- isMaintainer,
976
- _score
977
- };
978
- }).sort((a, b) => b._score - a._score);
979
- issue.topComments = comments.slice(0, 3).map(({ _score: _, ...c }) => c);
980
- if (issue.state === "closed") issue.resolvedIn = detectResolvedVersion(comments);
981
- }
982
- } catch {}
983
- }
984
- function detectResolvedVersion(comments) {
985
- const maintainerComments = comments.filter((c) => c.isMaintainer);
986
- for (const c of maintainerComments.reverse()) {
987
- const match = c.body.match(/(?:fixed|landed|released|available|shipped|resolved|included)\s+in\s+v?(\d+\.\d+(?:\.\d+)?)/i);
988
- if (match) return match[1];
989
- if (c.body.length < 100) {
990
- const vMatch = c.body.match(/\bv?(\d+\.\d+\.\d+)\b/);
991
- if (vMatch) return vMatch[1];
992
- }
993
- }
994
- }
995
- async function fetchGitHubIssues(owner, repo, limit = 30, releasedAt, fromDate) {
996
- if (!isGhAvailable()) return [];
997
- const openCount = Math.ceil(limit * .75);
998
- const closedCount = limit - openCount;
999
- try {
1000
- const open = fetchIssuesByState(owner, repo, "open", Math.min(openCount * 2, 100), releasedAt, fromDate);
1001
- const closed = fetchIssuesByState(owner, repo, "closed", Math.min(closedCount * 2, 50), releasedAt, fromDate);
1002
- const selected = applyTypeQuotas([...open, ...closed], limit);
1003
- enrichWithComments(owner, repo, selected);
1004
- return selected;
1005
- } catch {
1006
- return [];
1007
- }
1008
- }
1009
- function formatIssueAsMarkdown(issue) {
1010
- const limit = bodyLimit(issue.reactions);
1011
- const fmFields = {
1012
- number: issue.number,
1013
- title: issue.title,
1014
- type: issue.type,
1015
- state: issue.state,
1016
- created: isoDate(issue.createdAt),
1017
- url: issue.url,
1018
- reactions: issue.reactions,
1019
- comments: issue.comments
1020
- };
1021
- if (issue.resolvedIn) fmFields.resolvedIn = issue.resolvedIn;
1022
- if (issue.labels.length > 0) fmFields.labels = `[${issue.labels.join(", ")}]`;
1023
- const lines = [
1024
- buildFrontmatter(fmFields),
1025
- "",
1026
- `# ${issue.title}`
1027
- ];
1028
- if (issue.body) {
1029
- const body = truncateBody(issue.body, limit);
1030
- lines.push("", body);
1031
- }
1032
- if (issue.topComments.length > 0) {
1033
- lines.push("", "---", "", "## Top Comments");
1034
- for (const c of issue.topComments) {
1035
- const reactions = c.reactions > 0 ? ` (+${c.reactions})` : "";
1036
- const maintainer = c.isMaintainer ? " [maintainer]" : "";
1037
- const commentBody = truncateBody(c.body, 600);
1038
- lines.push("", `**@${c.author}**${maintainer}${reactions}:`, "", commentBody);
1039
- }
1040
- }
1041
- return lines.join("\n");
1042
- }
1043
- function generateIssueIndex(issues) {
1044
- const byType = /* @__PURE__ */ new Map();
1045
- for (const issue of issues) mapInsert(byType, issue.type, () => []).push(issue);
1046
- const typeLabels = {
1047
- bug: "Bugs & Regressions",
1048
- question: "Questions & Usage Help",
1049
- docs: "Documentation",
1050
- feature: "Feature Requests",
1051
- other: "Other"
1052
- };
1053
- const typeOrder = [
1054
- "bug",
1055
- "question",
1056
- "docs",
1057
- "other",
1058
- "feature"
1059
- ];
1060
- const sections = [
1061
- [
1062
- "---",
1063
- `total: ${issues.length}`,
1064
- `open: ${issues.filter((i) => i.state === "open").length}`,
1065
- `closed: ${issues.filter((i) => i.state !== "open").length}`,
1066
- "---"
1067
- ].join("\n"),
1068
- "",
1069
- "# Issues Index",
1070
- ""
1071
- ];
1072
- for (const type of typeOrder) {
1073
- const group = byType.get(type);
1074
- if (!group?.length) continue;
1075
- sections.push(`## ${typeLabels[type]} (${group.length})`, "");
1076
- for (const issue of group) {
1077
- const reactions = issue.reactions > 0 ? ` (+${issue.reactions})` : "";
1078
- const state = issue.state === "open" ? "" : " [closed]";
1079
- const resolved = issue.resolvedIn ? ` [fixed in ${issue.resolvedIn}]` : "";
1080
- const date = isoDate(issue.createdAt);
1081
- sections.push(`- [#${issue.number}](./issue-${issue.number}.md): ${issue.title}${reactions}${state}${resolved} (${date})`);
1082
- }
1083
- sections.push("");
1084
- }
1085
- return sections.join("\n");
1086
- }
1087
- async function fetchLlmsUrl(docsUrl) {
1088
- const llmsUrl = `${new URL(docsUrl).origin}/llms.txt`;
1089
- if (await verifyUrl(llmsUrl)) return llmsUrl;
1090
- return null;
1091
- }
1092
- async function fetchLlmsTxt(url) {
1093
- const content = await fetchText(url);
1094
- if (!content || content.length < 50) return null;
1095
- return {
1096
- raw: content,
1097
- links: parseMarkdownLinks(content)
1098
- };
1099
- }
1100
- function parseMarkdownLinks(content) {
1101
- return extractLinks(content).filter((l) => l.url.endsWith(".md"));
1102
- }
1103
- function isSafeUrl(url) {
1104
- try {
1105
- const parsed = new URL(url);
1106
- if (parsed.protocol !== "https:") return false;
1107
- const host = parsed.hostname;
1108
- if (host === "localhost" || host === "0.0.0.0" || host === "[::1]") return false;
1109
- if (/^(?:127\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/.test(host)) return false;
1110
- if (/^\[(?:f[cd]|fe[89ab]|::ffff:)/i.test(host)) return false;
1111
- return true;
1112
- } catch {
1113
- return false;
1114
- }
1115
- }
1116
- async function downloadLlmsDocs(llmsContent, baseUrl, onProgress) {
1117
- const limit = pLimit(5);
1118
- let completed = 0;
1119
- return (await Promise.all(llmsContent.links.map((link) => limit(async () => {
1120
- const url = link.url.startsWith("http") ? link.url : `${baseUrl.replace(/\/$/, "")}${link.url.startsWith("/") ? "" : "/"}${link.url}`;
1121
- if (!isSafeUrl(url)) return null;
1122
- const content = await fetchText(url);
1123
- onProgress?.(link.url, ++completed, llmsContent.links.length);
1124
- if (content && content.length > 100) return {
1125
- url: link.url.startsWith("http") ? new URL(link.url).pathname : link.url,
1126
- title: link.title,
1127
- content
1128
- };
1129
- return null;
1130
- })))).filter((d) => d !== null);
1131
- }
1132
- function normalizeLlmsLinks(content, baseUrl) {
1133
- let normalized = content;
1134
- if (baseUrl) {
1135
- const escaped = baseUrl.replace(/\/$/, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1136
- normalized = normalized.replace(new RegExp(`\\]\\(${escaped}(/[^)]+\\.md)\\)`, "g"), "](./docs$1)");
1137
- }
1138
- normalized = normalized.replace(/\]\(\/([^)]+\.md)\)/g, "](./docs/$1)");
1139
- return normalized;
1140
- }
1141
- function extractSections(content, patterns) {
1142
- const sections = [];
1143
- const parts = content.split(/\n---\n/);
1144
- for (const part of parts) {
1145
- const urlMatch = part.match(/^url: *(\S.*)$/m);
1146
- if (!urlMatch) continue;
1147
- const url = urlMatch[1];
1148
- if (patterns.some((p) => url.includes(p))) {
1149
- const contentStart = part.indexOf("\n", part.indexOf("url:"));
1150
- if (contentStart > -1) sections.push(part.slice(contentStart + 1));
1151
- }
1152
- }
1153
- if (sections.length === 0) return null;
1154
- return sections.join("\n\n---\n\n");
1155
- }
1156
- async function verifyNpmRepo(owner, repo, packageName) {
1157
- const base = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD`;
1158
- const paths = [
1159
- "package.json",
1160
- `packages/${packageName.replace(/^@.*\//, "")}/package.json`,
1161
- `packages/${packageName.replace(/^@/, "").replace("/", "-")}/package.json`
1162
- ];
1163
- for (const path of paths) {
1164
- const text = await fetchGitHubRaw(`${base}/${path}`);
1165
- if (!text) continue;
1166
- try {
1167
- if (JSON.parse(text).name === packageName) return true;
1168
- } catch {}
1169
- }
1170
- return false;
1171
- }
1172
- async function searchGitHubRepo(packageName) {
1173
- const shortName = packageName.replace(/^@.*\//, "");
1174
- for (const candidate of [packageName.replace(/^@/, "").replace("/", "/"), shortName]) {
1175
- if (!candidate.includes("/")) {
1176
- if ((await $fetch.raw(`https://ungh.cc/repos/${shortName}/${shortName}`).catch(() => null))?.ok) return `https://github.com/${shortName}/${shortName}`;
1177
- continue;
1178
- }
1179
- if ((await $fetch.raw(`https://ungh.cc/repos/${candidate}`).catch(() => null))?.ok) return `https://github.com/${candidate}`;
1180
- }
1181
- const searchTerm = packageName.replace(/^@/, "");
1182
- if (isGhAvailable()) try {
1183
- const { stdout: json } = spawnSync("gh", [
1184
- "search",
1185
- "repos",
1186
- searchTerm,
1187
- "--json",
1188
- "fullName",
1189
- "--limit",
1190
- "5"
1191
- ], {
1192
- encoding: "utf-8",
1193
- timeout: 15e3
1194
- });
1195
- if (!json) throw new Error("no output");
1196
- const repos = JSON.parse(json);
1197
- const match = repos.find((r) => r.fullName.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.fullName.toLowerCase().endsWith(`/${shortName.toLowerCase()}`));
1198
- if (match) return `https://github.com/${match.fullName}`;
1199
- for (const candidate of repos) {
1200
- const gh = parseGitHubUrl(`https://github.com/${candidate.fullName}`);
1201
- if (gh && await verifyNpmRepo(gh.owner, gh.repo, packageName)) return `https://github.com/${candidate.fullName}`;
1202
- }
1203
- } catch {}
1204
- const data = await $fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchTerm} in:name`)}&per_page=5`).catch(() => null);
1205
- if (!data?.items?.length) return null;
1206
- const match = data.items.find((r) => r.full_name.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.full_name.toLowerCase().endsWith(`/${shortName.toLowerCase()}`));
1207
- if (match) return `https://github.com/${match.full_name}`;
1208
- for (const candidate of data.items) {
1209
- const gh = parseGitHubUrl(`https://github.com/${candidate.full_name}`);
1210
- if (gh && await verifyNpmRepo(gh.owner, gh.repo, packageName)) return `https://github.com/${candidate.full_name}`;
1211
- }
1212
- return null;
1213
- }
1214
- async function fetchGitHubRepoMeta(owner, repo, packageName) {
1215
- const override = packageName ? getDocOverride(packageName) : void 0;
1216
- if (override?.homepage) return { homepage: override.homepage };
1217
- const data = await ghApi(`repos/${owner}/${repo}`) ?? await $fetch(`https://api.github.com/repos/${owner}/${repo}`).catch(() => null);
1218
- return data?.homepage ? { homepage: data.homepage } : null;
1219
- }
1220
- async function fetchReadme(owner, repo, subdir, ref) {
1221
- const branch = ref || "main";
1222
- if (!isKnownPrivateRepo(owner, repo)) {
1223
- const unghUrl = subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/${branch}/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme${ref ? `?ref=${ref}` : ""}`;
1224
- if ((await $fetch.raw(unghUrl).catch(() => null))?.ok) return `ungh://${owner}/${repo}${subdir ? `/${subdir}` : ""}${ref ? `@${ref}` : ""}`;
1225
- }
1226
- const basePath = subdir ? `${subdir}/` : "";
1227
- const branches = ref ? [ref] : ["main", "master"];
1228
- const token = isKnownPrivateRepo(owner, repo) ? getGitHubToken() : null;
1229
- const authHeaders = token ? { Authorization: `token ${token}` } : {};
1230
- for (const b of branches) for (const filename of [
1231
- "README.md",
1232
- "Readme.md",
1233
- "readme.md"
1234
- ]) {
1235
- const readmeUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${b}/${basePath}${filename}`;
1236
- if ((await $fetch.raw(readmeUrl, { headers: authHeaders }).catch(() => null))?.ok) return readmeUrl;
1237
- }
1238
- const refParam = ref ? `?ref=${ref}` : "";
1239
- const apiData = await ghApi(subdir ? `repos/${owner}/${repo}/contents/${subdir}/README.md${refParam}` : `repos/${owner}/${repo}/readme${refParam}`);
1240
- if (apiData?.download_url) {
1241
- markRepoPrivate(owner, repo);
1242
- return apiData.download_url;
1243
- }
1244
- return null;
1245
- }
1246
- async function fetchReadmeContent(url) {
1247
- if (url.startsWith("file://")) {
1248
- const filePath = fileURLToPath(url);
1249
- if (!existsSync(filePath)) return null;
1250
- return readFileSync(filePath, "utf-8");
1251
- }
1252
- if (url.startsWith("ungh://")) {
1253
- let path = url.replace("ungh://", "");
1254
- let ref = "main";
1255
- const atIdx = path.lastIndexOf("@");
1256
- if (atIdx !== -1) {
1257
- ref = path.slice(atIdx + 1);
1258
- path = path.slice(0, atIdx);
1259
- }
1260
- const parts = path.split("/");
1261
- const owner = parts[0];
1262
- const repo = parts[1];
1263
- const subdir = parts.slice(2).join("/");
1264
- 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);
1265
- if (!text) return null;
1266
- try {
1267
- const json = JSON.parse(text);
1268
- return json.markdown || json.file?.contents || null;
1269
- } catch {
1270
- return text;
1271
- }
1272
- }
1273
- if (url.includes("raw.githubusercontent.com")) return fetchGitHubRaw(url);
1274
- return fetchText(url);
1275
- }
1276
- async function resolveGitHubRepo(owner, repo, onProgress) {
1277
- onProgress?.("Fetching repo metadata");
1278
- const repoUrl = `https://github.com/${owner}/${repo}`;
1279
- const meta = await ghApi(`repos/${owner}/${repo}`) ?? await $fetch(`https://api.github.com/repos/${owner}/${repo}`).catch(() => null);
1280
- const homepage = meta?.homepage || void 0;
1281
- const description = meta?.description || void 0;
1282
- onProgress?.("Fetching latest release");
1283
- const releases = await fetchUnghReleases(owner, repo);
1284
- let version = "main";
1285
- let releasedAt;
1286
- const latestRelease = releases[0];
1287
- if (latestRelease) {
1288
- version = latestRelease.tag.replace(/^v/, "");
1289
- releasedAt = latestRelease.publishedAt;
1290
- }
1291
- onProgress?.("Resolving docs");
1292
- const gitDocs = await fetchGitDocs(owner, repo, version);
1293
- const gitDocsUrl = gitDocs ? `${repoUrl}/tree/${gitDocs.ref}/docs` : void 0;
1294
- const gitRef = gitDocs?.ref;
1295
- onProgress?.("Fetching README");
1296
- const readmeUrl = await fetchReadme(owner, repo);
1297
- let llmsUrl;
1298
- if (homepage) {
1299
- onProgress?.("Checking llms.txt");
1300
- llmsUrl = await fetchLlmsUrl(homepage).catch(() => null) ?? void 0;
1301
- }
1302
- if (!gitDocsUrl && !readmeUrl && !llmsUrl) return null;
1303
- return {
1304
- name: repo,
1305
- version: latestRelease ? version : void 0,
1306
- releasedAt,
1307
- description,
1308
- repoUrl,
1309
- docsUrl: homepage,
1310
- gitDocsUrl,
1311
- gitRef,
1312
- gitDocsFallback: gitDocs?.fallback,
1313
- readmeUrl: readmeUrl ?? void 0,
1314
- llmsUrl
1315
- };
1316
- }
1317
- const VALID_CRATE_NAME = /^[a-z0-9][\w-]*$/;
1318
- const runCratesApiRateLimited = createRateLimitedRunner(1e3);
1319
- function selectCrateVersion(data, requestedVersion) {
1320
- const versions = data.versions || [];
1321
- if (requestedVersion) {
1322
- const exact = versions.find((v) => v.num === requestedVersion && !v.yanked);
1323
- if (exact?.num) return {
1324
- version: exact.num,
1325
- entry: exact
1326
- };
1327
- }
1328
- const crate = data.crate;
1329
- const preferred = [
1330
- crate?.max_stable_version,
1331
- crate?.newest_version,
1332
- crate?.max_version,
1333
- crate?.default_version
1334
- ].find(Boolean);
1335
- if (preferred) {
1336
- const match = versions.find((v) => v.num === preferred && !v.yanked);
1337
- if (match?.num) return {
1338
- version: preferred,
1339
- entry: match
1340
- };
1341
- if (versions.length === 0) return { version: preferred };
1342
- }
1343
- const firstStable = versions.find((v) => !v.yanked && v.num);
1344
- if (firstStable?.num) return {
1345
- version: firstStable.num,
1346
- entry: firstStable
1347
- };
1348
- return null;
1349
- }
1350
- function pickPreferredUrl(...urls) {
1351
- return urls.map((v) => v?.trim()).find((v) => !!v);
1352
- }
1353
- async function fetchCratesApi(url) {
1354
- return runCratesApiRateLimited(() => $fetch(url).catch(() => null));
1355
- }
1356
- async function resolveCrateDocsWithAttempts(crateName, options = {}) {
1357
- const attempts = [];
1358
- const onProgress = options.onProgress;
1359
- const normalizedName = crateName.trim().toLowerCase();
1360
- if (!normalizedName || !VALID_CRATE_NAME.test(normalizedName)) {
1361
- attempts.push({
1362
- source: "crates",
1363
- status: "error",
1364
- message: `Invalid crate name: ${crateName}`
1365
- });
1366
- return {
1367
- package: null,
1368
- attempts
1369
- };
1370
- }
1371
- onProgress?.("crates.io metadata");
1372
- const apiUrl = `https://crates.io/api/v1/crates/${encodeURIComponent(normalizedName)}`;
1373
- const data = await fetchCratesApi(apiUrl);
1374
- if (!data?.crate) {
1375
- attempts.push({
1376
- source: "crates",
1377
- url: apiUrl,
1378
- status: "not-found",
1379
- message: "Crate not found on crates.io"
1380
- });
1381
- return {
1382
- package: null,
1383
- attempts
1384
- };
1385
- }
1386
- attempts.push({
1387
- source: "crates",
1388
- url: apiUrl,
1389
- status: "success",
1390
- message: `Found crate: ${data.crate.name || normalizedName}`
1391
- });
1392
- const selected = selectCrateVersion(data, options.version);
1393
- if (!selected) {
1394
- attempts.push({
1395
- source: "crates",
1396
- url: apiUrl,
1397
- status: "error",
1398
- message: "No usable crate versions found"
1399
- });
1400
- return {
1401
- package: null,
1402
- attempts
1403
- };
1404
- }
1405
- const version = selected.version;
1406
- const versionEntry = selected.entry;
1407
- const docsRsUrl = `https://docs.rs/${encodeURIComponent(normalizedName)}/${encodeURIComponent(version)}`;
1408
- const repositoryRaw = pickPreferredUrl(versionEntry?.repository, data.crate.repository);
1409
- const homepage = pickPreferredUrl(versionEntry?.homepage, data.crate.homepage);
1410
- const documentation = pickPreferredUrl(versionEntry?.documentation, data.crate.documentation);
1411
- const normalizedRepo = repositoryRaw ? normalizeRepoUrl(repositoryRaw) : void 0;
1412
- const repoUrl = normalizedRepo && isLikelyCodeHostUrl(normalizedRepo) ? normalizedRepo : isLikelyCodeHostUrl(homepage) ? homepage : void 0;
1413
- let resolved = {
1414
- name: normalizedName,
1415
- version,
1416
- releasedAt: versionEntry?.created_at || data.crate.updated_at || void 0,
1417
- description: versionEntry?.description || data.crate.description,
1418
- docsUrl: (() => {
1419
- if (documentation && !isUselessDocsUrl(documentation) && !isLikelyCodeHostUrl(documentation)) return documentation;
1420
- if (homepage && !isUselessDocsUrl(homepage) && !isLikelyCodeHostUrl(homepage)) return homepage;
1421
- return docsRsUrl;
1422
- })(),
1423
- repoUrl
1424
- };
1425
- const gh = repoUrl ? parseGitHubUrl(repoUrl) : null;
1426
- if (gh) {
1427
- onProgress?.("GitHub enrichment");
1428
- const ghResolved = await resolveGitHubRepo(gh.owner, gh.repo);
1429
- if (ghResolved) {
1430
- attempts.push({
1431
- source: "github-meta",
1432
- url: repoUrl,
1433
- status: "success",
1434
- message: "Enriched via GitHub repo metadata"
1435
- });
1436
- resolved = {
1437
- ...ghResolved,
1438
- name: normalizedName,
1439
- version,
1440
- releasedAt: resolved.releasedAt || ghResolved.releasedAt,
1441
- description: resolved.description || ghResolved.description,
1442
- docsUrl: resolved.docsUrl || ghResolved.docsUrl,
1443
- repoUrl,
1444
- readmeUrl: ghResolved.readmeUrl || resolved.readmeUrl
1445
- };
1446
- } else attempts.push({
1447
- source: "github-meta",
1448
- url: repoUrl,
1449
- status: "not-found",
1450
- message: "GitHub enrichment failed, using crates.io metadata"
1451
- });
1452
- }
1453
- if (!resolved.llmsUrl && resolved.docsUrl) {
1454
- onProgress?.("llms.txt discovery");
1455
- resolved.llmsUrl = await fetchLlmsUrl(resolved.docsUrl).catch(() => null) ?? void 0;
1456
- if (resolved.llmsUrl) attempts.push({
1457
- source: "llms.txt",
1458
- url: resolved.llmsUrl,
1459
- status: "success"
1460
- });
1461
- }
1462
- return {
1463
- package: resolved,
1464
- attempts
1465
- };
1466
- }
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(/\/$/, "") || "/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(/[_.:-]/)[0]?.toLowerCase() || "";
1555
- return code.length >= 2 ? code.slice(0, 2) : "en";
1556
- }
1557
- function toCrawlPattern(docsUrl) {
1558
- return `${docsUrl.replace(/\/+$/, "")}/**`;
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
- function generateDocsIndex(docs) {
1737
- 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));
1738
- if (docFiles.length === 0) return "";
1739
- const rootFiles = [];
1740
- const byDir = /* @__PURE__ */ new Map();
1741
- for (const doc of docFiles) {
1742
- const rel = doc.path.slice(5);
1743
- const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "";
1744
- if (!dir) rootFiles.push(doc);
1745
- else {
1746
- const list = byDir.get(dir);
1747
- if (list) list.push(doc);
1748
- else byDir.set(dir, [doc]);
1749
- }
1750
- }
1751
- const sections = [
1752
- "---",
1753
- `total: ${docFiles.length}`,
1754
- "---",
1755
- "",
1756
- "# Docs Index",
1757
- ""
1758
- ];
1759
- for (const file of rootFiles) {
1760
- const rel = file.path.slice(5);
1761
- const title = extractTitle(file.content) || rel.replace(/\.md$/, "");
1762
- const desc = extractDescription(file.content);
1763
- const descPart = desc ? `: ${desc}` : "";
1764
- sections.push(`- [${title}](./${rel})${descPart}`);
1765
- }
1766
- if (rootFiles.length > 0) sections.push("");
1767
- for (const [dir, files] of byDir) {
1768
- sections.push(`## ${dir} (${files.length})`, "");
1769
- for (const file of files) {
1770
- const rel = file.path.slice(5);
1771
- const title = extractTitle(file.content) || rel.replace(/\.md$/, "").split("/").pop();
1772
- const desc = extractDescription(file.content);
1773
- const descPart = desc ? `: ${desc}` : "";
1774
- sections.push(`- [${title}](./${rel})${descPart}`);
1775
- }
1776
- sections.push("");
1777
- }
1778
- return sections.join("\n");
1779
- }
1780
- const SKIP_DIRS = [
1781
- "node_modules",
1782
- "_vendor",
1783
- "__tests__",
1784
- "__mocks__",
1785
- "__fixtures__",
1786
- "test",
1787
- "tests",
1788
- "fixture",
1789
- "fixtures",
1790
- "locales",
1791
- "locale",
1792
- "i18n",
1793
- ".git"
1794
- ];
1795
- const SKIP_PATTERNS = [
1796
- "*.min.*",
1797
- "*.prod.*",
1798
- "*.global.*",
1799
- "*.browser.*",
1800
- "*.map",
1801
- "*.map.js",
1802
- "CHANGELOG*",
1803
- "LICENSE*",
1804
- "README*"
1805
- ];
1806
- const MAX_FILE_SIZE = 500 * 1024;
1807
- async function resolveEntryFiles(packageDir) {
1808
- if (!existsSync(join(packageDir, "package.json"))) return [];
1809
- const files = await glob(["**/*.d.{ts,mts,cts}"], {
1810
- cwd: packageDir,
1811
- ignore: [...SKIP_DIRS.map((d) => `**/${d}/**`), ...SKIP_PATTERNS],
1812
- absolute: false,
1813
- expandDirectories: false
1814
- });
1815
- const entries = [];
1816
- for (const file of files) {
1817
- const absPath = join(packageDir, file);
1818
- let content;
1819
- try {
1820
- content = readFileSync(absPath, "utf-8");
1821
- } catch {
1822
- continue;
1823
- }
1824
- if (content.length > MAX_FILE_SIZE) continue;
1825
- entries.push({
1826
- path: file,
1827
- content,
1828
- type: "types"
1829
- });
1830
- }
1831
- return entries;
1832
- }
1833
- function parseGitSkillInput(input) {
1834
- const trimmed = input.trim();
1835
- if (trimmed.startsWith("@")) return null;
1836
- if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/") || trimmed.startsWith("~")) return {
1837
- type: "local",
1838
- localPath: trimmed.startsWith("~") ? resolve(process.env.HOME || "", trimmed.slice(1)) : resolve(trimmed)
1839
- };
1840
- if (trimmed.startsWith("git@")) {
1841
- const gh = parseGitHubUrl(normalizeRepoUrl(trimmed));
1842
- if (gh) return {
1843
- type: "github",
1844
- owner: gh.owner,
1845
- repo: gh.repo
1846
- };
1847
- return null;
1848
- }
1849
- if (trimmed.startsWith("https://") || trimmed.startsWith("http://")) return parseGitUrl(trimmed);
1850
- if (/^[\w.-]+\/[\w.-]+$/.test(trimmed)) return {
1851
- type: "github",
1852
- owner: trimmed.split("/")[0],
1853
- repo: trimmed.split("/")[1]
1854
- };
1855
- return null;
1856
- }
1857
- function parseGitUrl(url) {
1858
- try {
1859
- const parsed = new URL(url);
1860
- if (parsed.hostname === "github.com" || parsed.hostname === "www.github.com") {
1861
- const parts = parsed.pathname.replace(/^\//, "").replace(/\.git$/, "").split("/");
1862
- const owner = parts[0];
1863
- const repo = parts[1];
1864
- if (!owner || !repo) return null;
1865
- if (parts[2] === "tree" && parts.length >= 4) return {
1866
- type: "github",
1867
- owner,
1868
- repo,
1869
- ref: parts[3],
1870
- skillPath: parts.length > 4 ? parts.slice(4).join("/") : void 0
1871
- };
1872
- return {
1873
- type: "github",
1874
- owner,
1875
- repo
1876
- };
1877
- }
1878
- if (parsed.hostname === "gitlab.com") {
1879
- const parts = parsed.pathname.replace(/^\//, "").replace(/\.git$/, "").split("/");
1880
- const owner = parts[0];
1881
- const repo = parts[1];
1882
- if (!owner || !repo) return null;
1883
- return {
1884
- type: "gitlab",
1885
- owner,
1886
- repo
1887
- };
1888
- }
1889
- return null;
1890
- } catch {
1891
- return null;
1892
- }
1893
- }
1894
- function parseSkillFrontmatterName(content) {
1895
- const fm = parseFrontmatter(content);
1896
- return {
1897
- name: fm.name,
1898
- description: fm.description
1899
- };
1900
- }
1901
- function findSkillDirs(root, prefix = "") {
1902
- const out = [];
1903
- if (!existsSync(root)) return out;
1904
- for (const entry of readdirSync(root, { withFileTypes: true })) {
1905
- if (!entry.isDirectory()) continue;
1906
- const dir = resolve(root, entry.name);
1907
- const repoPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1908
- if (existsSync(resolve(dir, "SKILL.md"))) out.push({
1909
- dir,
1910
- repoPath
1911
- });
1912
- else out.push(...findSkillDirs(dir, repoPath));
1913
- }
1914
- return out;
1915
- }
1916
- function collectFiles(dir, prefix = "") {
1917
- const files = [];
1918
- if (!existsSync(dir)) return files;
1919
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1920
- const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1921
- const fullPath = resolve(dir, entry.name);
1922
- if (entry.isDirectory()) files.push(...collectFiles(fullPath, relPath));
1923
- else if (entry.isFile()) files.push({
1924
- path: relPath,
1925
- content: readFileSync(fullPath, "utf-8")
1926
- });
1927
- }
1928
- return files;
1929
- }
1930
- async function fetchGitSkills(source, onProgress) {
1931
- if (source.type === "local") return fetchLocalSkills(source);
1932
- if (source.type === "github") return fetchGitHubSkills(source, onProgress);
1933
- if (source.type === "gitlab") return fetchGitLabSkills(source, onProgress);
1934
- return { skills: [] };
1935
- }
1936
- function fetchLocalSkills(source) {
1937
- const base = source.localPath;
1938
- if (!existsSync(base)) return { skills: [] };
1939
- const skills = [];
1940
- const skillsDir = resolve(base, "skills");
1941
- if (existsSync(skillsDir)) for (const { dir, repoPath } of findSkillDirs(skillsDir, "skills")) {
1942
- const skill = readLocalSkill(dir, repoPath);
1943
- if (skill) skills.push(skill);
1944
- }
1945
- if (skills.length === 0) {
1946
- const skill = readLocalSkill(base, "");
1947
- if (skill) skills.push(skill);
1948
- }
1949
- return { skills };
1950
- }
1951
- function readLocalSkill(dir, repoPath) {
1952
- const skillMdPath = resolve(dir, "SKILL.md");
1953
- if (!existsSync(skillMdPath)) return null;
1954
- const content = readFileSync(skillMdPath, "utf-8");
1955
- const frontmatter = parseSkillFrontmatterName(content);
1956
- const dirName = dir.split("/").pop();
1957
- const name = frontmatter.name || dirName;
1958
- const files = collectFiles(dir).filter((f) => f.path !== "SKILL.md");
1959
- return {
1960
- name,
1961
- description: frontmatter.description || "",
1962
- path: repoPath,
1963
- content,
1964
- files
1965
- };
1966
- }
1967
- async function fetchGitHubSkills(source, onProgress) {
1968
- const { owner, repo } = source;
1969
- if (!owner || !repo) return { skills: [] };
1970
- const ref = source.ref || "main";
1971
- const refs = ref === "main" ? ["main", "master"] : [ref];
1972
- for (const tryRef of refs) {
1973
- const skills = await downloadGitHubSkills(owner, repo, tryRef, source.skillPath, onProgress);
1974
- if (skills.length > 0) return { skills };
1975
- }
1976
- return { skills: [] };
1977
- }
1978
- async function downloadGitHubSkills(owner, repo, ref, skillPath, onProgress) {
1979
- const tempDir = join(tmpdir(), `skilld-${Date.now()}`);
1980
- try {
1981
- if (skillPath) {
1982
- onProgress?.(`Downloading ${owner}/${repo}/${skillPath}@${ref}`);
1983
- const { dir } = await downloadTemplate(`github:${owner}/${repo}/${skillPath}#${ref}`, {
1984
- dir: tempDir,
1985
- force: true,
1986
- auth: getGitHubToken() || void 0
1987
- });
1988
- const skill = readLocalSkill(dir, skillPath);
1989
- return skill ? [skill] : [];
1990
- }
1991
- onProgress?.(`Downloading ${owner}/${repo}/skills@${ref}`);
1992
- try {
1993
- const { dir } = await downloadTemplate(`github:${owner}/${repo}/skills#${ref}`, {
1994
- dir: tempDir,
1995
- force: true,
1996
- auth: getGitHubToken() || void 0
1997
- });
1998
- const skills = [];
1999
- for (const { dir: skillDir, repoPath } of findSkillDirs(dir, "skills")) {
2000
- const skill = readLocalSkill(skillDir, repoPath);
2001
- if (skill) skills.push(skill);
2002
- }
2003
- if (skills.length > 0) {
2004
- onProgress?.(`Found ${skills.length} skill(s)`);
2005
- return skills;
2006
- }
2007
- } catch {}
2008
- const content = await fetchGitHubRaw(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/SKILL.md`);
2009
- if (content) {
2010
- const fm = parseSkillFrontmatterName(content);
2011
- onProgress?.("Found 1 skill");
2012
- return [{
2013
- name: fm.name || repo,
2014
- description: fm.description || "",
2015
- path: "",
2016
- content,
2017
- files: []
2018
- }];
2019
- }
2020
- return [];
2021
- } catch {
2022
- return [];
2023
- } finally {
2024
- rmSync(tempDir, {
2025
- recursive: true,
2026
- force: true
2027
- });
2028
- }
2029
- }
2030
- async function fetchGitLabSkills(source, onProgress) {
2031
- const { owner, repo } = source;
2032
- if (!owner || !repo) return { skills: [] };
2033
- const ref = source.ref || "main";
2034
- const tempDir = join(tmpdir(), `skilld-gitlab-${Date.now()}`);
2035
- try {
2036
- const subdir = source.skillPath || "skills";
2037
- onProgress?.(`Downloading ${owner}/${repo}/${subdir}@${ref}`);
2038
- const { dir } = await downloadTemplate(`gitlab:${owner}/${repo}/${subdir}#${ref}`, {
2039
- dir: tempDir,
2040
- force: true
2041
- });
2042
- if (source.skillPath) {
2043
- const skill = readLocalSkill(dir, source.skillPath);
2044
- return { skills: skill ? [skill] : [] };
2045
- }
2046
- const skills = [];
2047
- for (const { dir: skillDir, repoPath } of findSkillDirs(dir, "skills")) {
2048
- const skill = readLocalSkill(skillDir, repoPath);
2049
- if (skill) skills.push(skill);
2050
- }
2051
- if (skills.length > 0) {
2052
- onProgress?.(`Found ${skills.length} skill(s)`);
2053
- return { skills };
2054
- }
2055
- const content = await $fetch(`https://gitlab.com/${owner}/${repo}/-/raw/${ref}/SKILL.md`, { responseType: "text" }).catch(() => null);
2056
- if (content) {
2057
- const fm = parseSkillFrontmatterName(content);
2058
- return { skills: [{
2059
- name: fm.name || repo,
2060
- description: fm.description || "",
2061
- path: "",
2062
- content,
2063
- files: []
2064
- }] };
2065
- }
2066
- return { skills: [] };
2067
- } catch {
2068
- return { skills: [] };
2069
- } finally {
2070
- rmSync(tempDir, {
2071
- recursive: true,
2072
- force: true
2073
- });
2074
- }
2075
- }
2076
- async function resolveLocalDep(packageName, cwd) {
2077
- const result = readPackageJsonSafe(join(cwd, "package.json"));
2078
- if (!result) return null;
2079
- const pkg = result.parsed;
2080
- const depVersion = {
2081
- ...pkg.dependencies,
2082
- ...pkg.devDependencies
2083
- }[packageName];
2084
- if (!depVersion?.startsWith("link:")) return null;
2085
- return resolveLocalPackageDocs(resolve(cwd, depVersion.slice(5)));
2086
- }
2087
- async function searchNpmPackages(query, size = 5) {
2088
- const data = await $fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`).catch(() => null);
2089
- if (!data?.objects?.length) return [];
2090
- return data.objects.map((o) => ({
2091
- name: o.package.name,
2092
- description: o.package.description,
2093
- version: o.package.version
2094
- }));
2095
- }
2096
- async function fetchNpmPackage(packageName) {
2097
- const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
2098
- if (data) return data;
2099
- return $fetch(`https://registry.npmjs.org/${packageName}/latest`).catch(() => null);
2100
- }
2101
- async function fetchNpmRegistryMeta(packageName, version) {
2102
- const { name: barePackageName } = parsePackageSpec(packageName);
2103
- const data = await $fetch(`https://registry.npmjs.org/${barePackageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null);
2104
- if (!data) return {};
2105
- const distTags = data["dist-tags"] ? Object.fromEntries(Object.entries(data["dist-tags"]).map(([tag, ver]) => [tag, {
2106
- version: ver,
2107
- releasedAt: data.time?.[ver]
2108
- }])) : void 0;
2109
- return {
2110
- releasedAt: data.time?.[version] || void 0,
2111
- distTags
2112
- };
2113
- }
2114
- async function resolveGitHub(gh, targetVersion, pkg, result, attempts, onProgress, opts) {
2115
- let allFiles;
2116
- if (targetVersion) {
2117
- onProgress?.("github-docs");
2118
- const gitDocs = await fetchGitDocs(gh.owner, gh.repo, targetVersion, pkg.name, opts?.rawRepoUrl);
2119
- if (gitDocs) {
2120
- result.gitDocsUrl = gitDocs.baseUrl;
2121
- result.gitRef = gitDocs.ref;
2122
- result.gitDocsFallback = gitDocs.fallback;
2123
- allFiles = gitDocs.allFiles;
2124
- attempts.push({
2125
- source: "github-docs",
2126
- url: gitDocs.baseUrl,
2127
- status: "success",
2128
- message: gitDocs.fallback ? `Found ${gitDocs.files.length} docs at ${gitDocs.ref} (no tag for v${targetVersion})` : `Found ${gitDocs.files.length} docs at ${gitDocs.ref}`
2129
- });
2130
- } else attempts.push({
2131
- source: "github-docs",
2132
- url: `${result.repoUrl}/tree/v${targetVersion}/docs`,
2133
- status: "not-found",
2134
- message: "No docs/ folder found at version tag"
2135
- });
2136
- }
2137
- if (!result.docsUrl) {
2138
- onProgress?.("github-meta");
2139
- const repoMeta = await fetchGitHubRepoMeta(gh.owner, gh.repo, pkg.name);
2140
- if (repoMeta?.homepage && !isUselessDocsUrl(repoMeta.homepage)) {
2141
- result.docsUrl = repoMeta.homepage;
2142
- attempts.push({
2143
- source: "github-meta",
2144
- url: result.repoUrl,
2145
- status: "success",
2146
- message: `Found homepage: ${repoMeta.homepage}`
2147
- });
2148
- } else attempts.push({
2149
- source: "github-meta",
2150
- url: result.repoUrl,
2151
- status: "not-found",
2152
- message: "No homepage in repo metadata"
2153
- });
2154
- }
2155
- onProgress?.("readme");
2156
- const readmeUrl = await fetchReadme(gh.owner, gh.repo, opts?.subdir, result.gitRef);
2157
- if (readmeUrl) {
2158
- result.readmeUrl = readmeUrl;
2159
- attempts.push({
2160
- source: "readme",
2161
- url: readmeUrl,
2162
- status: "success"
2163
- });
2164
- } else attempts.push({
2165
- source: "readme",
2166
- url: `${result.repoUrl}/README.md`,
2167
- status: "not-found",
2168
- message: "No README found"
2169
- });
2170
- return allFiles;
2171
- }
2172
- async function resolvePackageDocs(packageName, options = {}) {
2173
- return (await resolvePackageDocsWithAttempts(packageName, options)).package;
2174
- }
2175
- async function resolvePackageDocsWithAttempts(packageName, options = {}) {
2176
- const attempts = [];
2177
- const { onProgress } = options;
2178
- onProgress?.("npm");
2179
- const pkg = await fetchNpmPackage(packageName);
2180
- if (!pkg) {
2181
- attempts.push({
2182
- source: "npm",
2183
- url: `https://registry.npmjs.org/${packageName}/latest`,
2184
- status: "not-found",
2185
- message: "Package not found on npm registry"
2186
- });
2187
- return {
2188
- package: null,
2189
- attempts
2190
- };
2191
- }
2192
- attempts.push({
2193
- source: "npm",
2194
- url: `https://registry.npmjs.org/${packageName}/latest`,
2195
- status: "success",
2196
- message: `Found ${pkg.name}@${pkg.version}`
2197
- });
2198
- const registryMeta = pkg.version ? await fetchNpmRegistryMeta(packageName, pkg.version) : {};
2199
- const result = {
2200
- name: pkg.name,
2201
- version: pkg.version,
2202
- releasedAt: registryMeta.releasedAt,
2203
- description: pkg.description,
2204
- dependencies: pkg.dependencies,
2205
- distTags: registryMeta.distTags
2206
- };
2207
- let gitDocsAllFiles;
2208
- let subdir;
2209
- let rawRepoUrl;
2210
- if (typeof pkg.repository === "object" && pkg.repository?.url) {
2211
- rawRepoUrl = pkg.repository.url;
2212
- const normalized = normalizeRepoUrl(rawRepoUrl);
2213
- if (!normalized.includes("://") && normalized.includes("/") && !normalized.includes(":")) result.repoUrl = `https://github.com/${normalized}`;
2214
- else result.repoUrl = normalized;
2215
- subdir = pkg.repository.directory;
2216
- } else if (typeof pkg.repository === "string") if (pkg.repository.includes("://")) {
2217
- const gh = parseGitHubUrl(pkg.repository);
2218
- if (gh) result.repoUrl = `https://github.com/${gh.owner}/${gh.repo}`;
2219
- } else {
2220
- const repo = pkg.repository.replace(/^github:/, "");
2221
- if (repo.includes("/") && !repo.includes(":")) result.repoUrl = `https://github.com/${repo}`;
2222
- }
2223
- if (pkg.homepage && !isGitHubRepoUrl(pkg.homepage) && !isUselessDocsUrl(pkg.homepage)) result.docsUrl = pkg.homepage;
2224
- if (result.repoUrl?.includes("github.com")) {
2225
- const gh = parseGitHubUrl(result.repoUrl);
2226
- if (gh) gitDocsAllFiles = await resolveGitHub(gh, options.version || pkg.version, pkg, result, attempts, onProgress, {
2227
- rawRepoUrl,
2228
- subdir
2229
- });
2230
- } else if (!result.repoUrl) {
2231
- onProgress?.("github-search");
2232
- const searchedUrl = await searchGitHubRepo(pkg.name);
2233
- if (searchedUrl) {
2234
- result.repoUrl = searchedUrl;
2235
- attempts.push({
2236
- source: "github-search",
2237
- url: searchedUrl,
2238
- status: "success",
2239
- message: `Found via GitHub search: ${searchedUrl}`
2240
- });
2241
- const gh = parseGitHubUrl(searchedUrl);
2242
- if (gh) gitDocsAllFiles = await resolveGitHub(gh, options.version || pkg.version, pkg, result, attempts, onProgress);
2243
- } else attempts.push({
2244
- source: "github-search",
2245
- status: "not-found",
2246
- message: "No repository URL in package.json and GitHub search found no match"
2247
- });
2248
- }
2249
- const crawlUrl = getCrawlUrl(packageName);
2250
- if (crawlUrl) result.crawlUrl = crawlUrl;
2251
- if (result.docsUrl) {
2252
- onProgress?.("llms.txt");
2253
- const llmsUrl = await fetchLlmsUrl(result.docsUrl);
2254
- if (llmsUrl) {
2255
- result.llmsUrl = llmsUrl;
2256
- attempts.push({
2257
- source: "llms.txt",
2258
- url: llmsUrl,
2259
- status: "success"
2260
- });
2261
- } else attempts.push({
2262
- source: "llms.txt",
2263
- url: `${new URL(result.docsUrl).origin}/llms.txt`,
2264
- status: "not-found",
2265
- message: "No llms.txt at docs URL"
2266
- });
2267
- }
2268
- if (result.gitDocsUrl && result.llmsUrl && gitDocsAllFiles) {
2269
- const llmsContent = await fetchLlmsTxt(result.llmsUrl);
2270
- if (llmsContent && llmsContent.links.length > 0) {
2271
- const validation = validateGitDocsWithLlms(llmsContent.links, gitDocsAllFiles);
2272
- if (!validation.isValid) {
2273
- attempts.push({
2274
- source: "github-docs",
2275
- url: result.gitDocsUrl,
2276
- status: "not-found",
2277
- message: `Heuristic git docs don't match llms.txt links (${Math.round(validation.matchRatio * 100)}% match), preferring llms.txt`
2278
- });
2279
- result.gitDocsUrl = void 0;
2280
- result.gitRef = void 0;
2281
- }
2282
- }
2283
- }
2284
- if (!result.docsUrl && !result.llmsUrl && !result.readmeUrl && !result.gitDocsUrl && options.cwd) {
2285
- onProgress?.("local");
2286
- const pkgDir = join(options.cwd, "node_modules", packageName);
2287
- const readmeFile = existsSync(pkgDir) && readdirSync(pkgDir).find((f) => /^readme\.md$/i.test(f));
2288
- if (readmeFile) {
2289
- const readmePath = join(pkgDir, readmeFile);
2290
- result.readmeUrl = pathToFileURL(readmePath).href;
2291
- attempts.push({
2292
- source: "readme",
2293
- url: readmePath,
2294
- status: "success",
2295
- message: "Found local readme in node_modules"
2296
- });
2297
- }
2298
- }
2299
- if (!result.docsUrl && !result.llmsUrl && !result.readmeUrl && !result.gitDocsUrl) return {
2300
- package: null,
2301
- attempts,
2302
- registryVersion: pkg.version
2303
- };
2304
- return {
2305
- package: result,
2306
- attempts,
2307
- registryVersion: pkg.version
2308
- };
2309
- }
2310
- function parseVersionSpecifier(name, version, cwd) {
2311
- if (version.startsWith("link:")) {
2312
- const linkedPkg = readPackageJsonSafe(join(resolve(cwd, version.slice(5)), "package.json"));
2313
- if (linkedPkg) return {
2314
- name: linkedPkg.parsed.name || name,
2315
- version: linkedPkg.parsed.version || "0.0.0"
2316
- };
2317
- return null;
2318
- }
2319
- if (version.startsWith("npm:")) {
2320
- const specifier = version.slice(4);
2321
- const atIndex = specifier.startsWith("@") ? specifier.indexOf("@", 1) : specifier.indexOf("@");
2322
- const realName = atIndex > 0 ? specifier.slice(0, atIndex) : specifier;
2323
- return {
2324
- name: realName,
2325
- version: resolveInstalledVersion(realName, cwd) || "*"
2326
- };
2327
- }
2328
- if (version.startsWith("file:") || version.startsWith("git:") || version.startsWith("git+")) return null;
2329
- const installed = resolveInstalledVersion(name, cwd);
2330
- if (installed) return {
2331
- name,
2332
- version: installed
2333
- };
2334
- if (/^[\^~>=<\d]/.test(version)) return {
2335
- name,
2336
- version: version.replace(/^[\^~>=<]+/, "")
2337
- };
2338
- if (version.startsWith("catalog:") || version.startsWith("workspace:")) return {
2339
- name,
2340
- version: "*"
2341
- };
2342
- return null;
2343
- }
2344
- function resolveInstalledVersion(name, cwd) {
2345
- try {
2346
- return readPackageJsonSafe(resolvePathSync(`${name}/package.json`, { url: cwd }))?.parsed.version || null;
2347
- } catch {
2348
- try {
2349
- let dir = dirname(resolvePathSync(name, { url: cwd }));
2350
- while (dir && basename(dir) !== "node_modules") {
2351
- const pkg = readPackageJsonSafe(join(dir, "package.json"));
2352
- if (pkg) return pkg.parsed.version || null;
2353
- const parent = dirname(dir);
2354
- if (parent === dir) break;
2355
- dir = parent;
2356
- }
2357
- } catch {}
2358
- return null;
2359
- }
2360
- }
2361
- async function readLocalDependencies(cwd) {
2362
- const result = readPackageJsonSafe(join(cwd, "package.json"));
2363
- if (!result) throw new Error("No package.json found in current directory");
2364
- const pkg = result.parsed;
2365
- const deps = {
2366
- ...pkg.dependencies,
2367
- ...pkg.devDependencies
2368
- };
2369
- const results = [];
2370
- for (const [name, version] of Object.entries(deps)) {
2371
- const parsed = parseVersionSpecifier(name, version, cwd);
2372
- if (parsed) results.push(parsed);
2373
- }
2374
- return results;
2375
- }
2376
- function readLocalPackageInfo(localPath) {
2377
- const result = readPackageJsonSafe(join(localPath, "package.json"));
2378
- if (!result) return null;
2379
- const pkg = result.parsed;
2380
- let repoUrl;
2381
- if (pkg.repository?.url) repoUrl = normalizeRepoUrl(pkg.repository.url);
2382
- else if (typeof pkg.repository === "string") repoUrl = normalizeRepoUrl(pkg.repository);
2383
- return {
2384
- name: pkg.name,
2385
- version: pkg.version || "0.0.0",
2386
- description: pkg.description,
2387
- repoUrl,
2388
- localPath
2389
- };
2390
- }
2391
- async function resolveLocalPackageDocs(localPath) {
2392
- const info = readLocalPackageInfo(localPath);
2393
- if (!info) return null;
2394
- const result = {
2395
- name: info.name,
2396
- version: info.version,
2397
- description: info.description,
2398
- repoUrl: info.repoUrl
2399
- };
2400
- if (info.repoUrl?.includes("github.com")) {
2401
- const gh = parseGitHubUrl(info.repoUrl);
2402
- if (gh) {
2403
- const gitDocs = await fetchGitDocs(gh.owner, gh.repo, info.version, info.name);
2404
- if (gitDocs) {
2405
- result.gitDocsUrl = gitDocs.baseUrl;
2406
- result.gitRef = gitDocs.ref;
2407
- result.gitDocsFallback = gitDocs.fallback;
2408
- }
2409
- const readmeUrl = await fetchReadme(gh.owner, gh.repo, void 0, result.gitRef);
2410
- if (readmeUrl) result.readmeUrl = readmeUrl;
2411
- }
2412
- }
2413
- if (!result.readmeUrl && !result.gitDocsUrl) {
2414
- const readmeFile = readdirSync(localPath).find((f) => /^readme\.md$/i.test(f));
2415
- if (readmeFile) result.readmeUrl = pathToFileURL(join(localPath, readmeFile)).href;
2416
- }
2417
- if (!result.readmeUrl && !result.gitDocsUrl) return null;
2418
- return result;
2419
- }
2420
- async function fetchPkgDist(name, version) {
2421
- const cacheDir = getCacheDir(name, version);
2422
- const pkgDir = join(cacheDir, "pkg");
2423
- if (existsSync(join(pkgDir, "package.json"))) return pkgDir;
2424
- const data = await $fetch(`https://registry.npmjs.org/${name}/${version}`).catch(() => null);
2425
- if (!data) return null;
2426
- const tarballUrl = data.dist?.tarball;
2427
- if (!tarballUrl) return null;
2428
- const tarballRes = await fetch(tarballUrl, { headers: { "User-Agent": SKILLD_USER_AGENT } }).catch(() => null);
2429
- if (!tarballRes?.ok || !tarballRes.body) return null;
2430
- mkdirSync(pkgDir, { recursive: true });
2431
- const tmpTarball = join(cacheDir, "_pkg.tgz");
2432
- const fileStream = createWriteStream(tmpTarball);
2433
- const fileClosed = new Promise((resolve) => fileStream.once("close", resolve));
2434
- const reader = tarballRes.body.getReader();
2435
- try {
2436
- await new Promise((res, reject) => {
2437
- const writable = new Writable({ write(chunk, _encoding, callback) {
2438
- fileStream.write(chunk, callback);
2439
- } });
2440
- writable.on("finish", () => {
2441
- fileStream.end();
2442
- });
2443
- fileStream.on("close", () => res());
2444
- writable.on("error", reject);
2445
- fileStream.on("error", reject);
2446
- function pump() {
2447
- reader.read().then(({ done, value }) => {
2448
- if (done) {
2449
- writable.end();
2450
- return;
2451
- }
2452
- writable.write(value, () => pump());
2453
- }).catch(reject);
2454
- }
2455
- pump();
2456
- });
2457
- const { status } = spawnSync("tar", [
2458
- "xzf",
2459
- tmpTarball,
2460
- "--strip-components=1",
2461
- "-C",
2462
- pkgDir
2463
- ], { stdio: "ignore" });
2464
- if (status !== 0) {
2465
- rmSync(pkgDir, {
2466
- recursive: true,
2467
- force: true
2468
- });
2469
- return null;
2470
- }
2471
- return pkgDir;
2472
- } catch {
2473
- rmSync(pkgDir, {
2474
- recursive: true,
2475
- force: true
2476
- });
2477
- return null;
2478
- } finally {
2479
- reader.cancel().catch(() => {});
2480
- fileStream.destroy();
2481
- await fileClosed;
2482
- try {
2483
- rmSync(tmpTarball, { force: true });
2484
- } catch {}
2485
- }
2486
- }
2487
- async function fetchLatestVersion(packageName) {
2488
- const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
2489
- if (data?.version) return data.version;
2490
- return (await $fetch(`https://registry.npmjs.org/${packageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null))?.["dist-tags"]?.latest || null;
2491
- }
2492
- function getInstalledSkillVersion(skillDir) {
2493
- const skillPath = join(skillDir, "SKILL.md");
2494
- if (!existsSync(skillPath)) return null;
2495
- return readFileSync(skillPath, "utf-8").match(/^version:\s*"?([^"\n]+)"?/m)?.[1] || null;
2496
- }
2497
- function parseSkillInput(input) {
2498
- const trimmed = input.trim();
2499
- if (trimmed.startsWith("npm:")) {
2500
- const { name, tag } = splitPackageTag(trimmed.slice(4));
2501
- return {
2502
- type: "npm",
2503
- package: name,
2504
- tag
2505
- };
2506
- }
2507
- if (trimmed.startsWith("crate:")) {
2508
- const rest = trimmed.slice(6).trim();
2509
- const atIdx = rest.indexOf("@");
2510
- return {
2511
- type: "crate",
2512
- package: (atIdx === -1 ? rest : rest.slice(0, atIdx)).toLowerCase(),
2513
- version: atIdx === -1 ? void 0 : rest.slice(atIdx + 1) || void 0
2514
- };
2515
- }
2516
- if (trimmed.startsWith("gh:") || trimmed.startsWith("github:")) {
2517
- const rest = trimmed.startsWith("gh:") ? trimmed.slice(3) : trimmed.slice(7);
2518
- const gitSource = parseGitSkillInput(rest);
2519
- if (gitSource) return {
2520
- type: "git",
2521
- source: gitSource
2522
- };
2523
- if (/^[\w.-]+\/[\w.-]+/.test(rest)) {
2524
- const [owner, repo] = rest.split("/");
2525
- return {
2526
- type: "git",
2527
- source: {
2528
- type: "github",
2529
- owner,
2530
- repo
2531
- }
2532
- };
2533
- }
2534
- return {
2535
- type: "bare",
2536
- package: rest
2537
- };
2538
- }
2539
- if (trimmed.startsWith("@")) {
2540
- const rest = trimmed.slice(1);
2541
- if (rest.indexOf("/") === -1) return {
2542
- type: "curator",
2543
- handle: rest
2544
- };
2545
- const { name, tag } = splitPackageTag(trimmed);
2546
- return {
2547
- type: "bare",
2548
- package: name,
2549
- tag
2550
- };
2551
- }
2552
- const gitSource = parseGitSkillInput(trimmed);
2553
- if (gitSource) return {
2554
- type: "git",
2555
- source: gitSource
2556
- };
2557
- const { name, tag } = splitPackageTag(trimmed);
2558
- return {
2559
- type: "bare",
2560
- package: name,
2561
- tag
2562
- };
2563
- }
2564
- function resolveSkillName(input) {
2565
- const source = parseSkillInput(input);
2566
- switch (source.type) {
2567
- case "npm":
2568
- case "bare": return source.package;
2569
- case "crate": return `crate:${source.package}`;
2570
- case "git":
2571
- if (source.source.type === "github" && source.source.repo) return source.source.repo;
2572
- return null;
2573
- case "curator":
2574
- case "collection": return null;
2575
- default: throw new Error(`Unhandled SkillSource type: ${JSON.stringify(source)}`);
2576
- }
2577
- }
2578
- function toStoragePackageName(identityName) {
2579
- if (identityName.startsWith("crate:")) return `@skilld-crate/${identityName.slice(6)}`;
2580
- return identityName;
2581
- }
2582
- function isCrateSpec(spec) {
2583
- return spec.startsWith("crate:");
2584
- }
2585
- function toCrateIdentity(crateName) {
2586
- return `crate:${crateName}`;
2587
- }
2588
- function splitPackageTag(spec) {
2589
- if (spec.startsWith("@")) {
2590
- const slashIdx = spec.indexOf("/");
2591
- if (slashIdx !== -1) {
2592
- const afterSlash = spec.indexOf("@", slashIdx);
2593
- if (afterSlash !== -1) return {
2594
- name: spec.slice(0, afterSlash),
2595
- tag: spec.slice(afterSlash + 1) || void 0
2596
- };
2597
- }
2598
- return { name: spec };
2599
- }
2600
- const atIdx = spec.indexOf("@");
2601
- if (atIdx !== -1) return {
2602
- name: spec.slice(0, atIdx),
2603
- tag: spec.slice(atIdx + 1) || void 0
2604
- };
2605
- return { name: spec };
2606
- }
2607
- const RESOLVE_STEP_LABELS = {
2608
- "npm": "npm registry",
2609
- "github-docs": "GitHub docs",
2610
- "github-meta": "GitHub meta",
2611
- "github-search": "GitHub search",
2612
- "readme": "README",
2613
- "llms.txt": "llms.txt",
2614
- "crawl": "website crawl",
2615
- "local": "node_modules"
2616
- };
2617
- async function resolvePackageOrCrate(packageSpec, opts) {
2618
- const { cwd, onProgress } = opts;
2619
- const isCrate = isCrateSpec(packageSpec);
2620
- const normalizedSpec = isCrate ? packageSpec.slice(6).trim() : packageSpec;
2621
- const { name: parsedName, tag: requestedTag } = parsePackageSpec(normalizedSpec);
2622
- const packageName = isCrate ? parsedName.toLowerCase() : parsedName;
2623
- const identityPackageName = isCrate ? toCrateIdentity(packageName) : packageName;
2624
- const storagePackageName = toStoragePackageName(identityPackageName);
2625
- const localDeps = isCrate ? [] : await readLocalDependencies(cwd).catch(() => []);
2626
- const localVersion = isCrate ? void 0 : localDeps.find((d) => d.name === packageName)?.version;
2627
- const resolveResult = isCrate ? await resolveCrateDocsWithAttempts(packageName, {
2628
- version: requestedTag,
2629
- onProgress
2630
- }) : await resolvePackageDocsWithAttempts(requestedTag ? normalizedSpec : packageName, {
2631
- version: localVersion,
2632
- cwd,
2633
- onProgress: (step) => onProgress?.(RESOLVE_STEP_LABELS[step] ?? step)
2634
- });
2635
- let resolved = resolveResult.package;
2636
- if (!resolved && !isCrate) {
2637
- onProgress?.(RESOLVE_STEP_LABELS.local);
2638
- resolved = await resolveLocalDep(packageName, cwd);
2639
- }
2640
- return {
2641
- packageName,
2642
- identityPackageName,
2643
- storagePackageName,
2644
- isCrate,
2645
- requestedTag,
2646
- localVersion,
2647
- resolved,
2648
- attempts: resolveResult.attempts,
2649
- registryVersion: resolveResult.registryVersion
2650
- };
2651
- }
2652
- export { fetchReleaseNotes as $, resolveCrateDocsWithAttempts as A, parseMarkdownLinks as B, resolveEntryFiles as C, generateDiscussionIndex as D, formatDiscussionAsMarkdown as E, downloadLlmsDocs as F, MIN_GIT_DOCS as G, formatIssueAsMarkdown as H, extractSections as I, isShallowGitDocs as J, fetchGitDocs as K, fetchLlmsTxt as L, fetchReadme as M, fetchReadmeContent as N, fetchCrawledDocs as O, resolveGitHubRepo as P, compareSemver as Q, fetchLlmsUrl as R, parseSkillFrontmatterName as S, fetchGitHubDiscussions as T, generateIssueIndex as U, fetchGitHubIssues as V, isGhAvailable as W, mapInsert as X, validateGitDocsWithLlms as Y, fetchBlogReleases as Z, resolvePackageDocsWithAttempts as _, toStoragePackageName as a, fetchGitHubRaw as at, fetchGitSkills as b, fetchNpmRegistryMeta as c, normalizeRepoUrl as ct, parseVersionSpecifier as d, parsePackageSpec as dt, generateReleaseIndex as et, readLocalDependencies as f, verifyUrl as ft, resolvePackageDocs as g, resolveLocalPackageDocs as h, resolveSkillName as i, extractBranchHint as it, fetchGitHubRepoMeta as j, toCrawlPattern as k, fetchPkgDist as l, parseGitHubRepoSlug as lt, resolveInstalledVersion as m, isCrateSpec as n, parseSemver as nt, fetchLatestVersion as o, fetchText as ot, readLocalPackageInfo as p, filterFrameworkDocs as q, parseSkillInput as r, $fetch as rt, fetchNpmPackage as s, isGitHubRepoUrl as st, resolvePackageOrCrate as t, isPrerelease as tt, getInstalledSkillVersion as u, parseGitHubUrl as ut, searchNpmPackages as v, generateDocsIndex as w, parseGitSkillInput as x, resolveLocalDep as y, normalizeLlmsLinks as z };
2653
-
2654
- //# sourceMappingURL=sources.mjs.map