skilld 1.5.5 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/_chunks/agent.mjs +0 -77
- package/dist/_chunks/agent.mjs.map +1 -1
- package/dist/_chunks/assemble.mjs +0 -17
- package/dist/_chunks/assemble.mjs.map +1 -1
- package/dist/_chunks/author.mjs +0 -18
- package/dist/_chunks/author.mjs.map +1 -1
- package/dist/_chunks/cache.mjs +0 -72
- package/dist/_chunks/cache.mjs.map +1 -1
- package/dist/_chunks/cache2.mjs +84 -17
- package/dist/_chunks/cache2.mjs.map +1 -1
- package/dist/_chunks/cli-helpers.mjs +0 -47
- package/dist/_chunks/cli-helpers.mjs.map +1 -1
- package/dist/_chunks/cli-helpers2.mjs +0 -11
- package/dist/_chunks/config.mjs +0 -27
- package/dist/_chunks/config.mjs.map +1 -1
- package/dist/_chunks/core.mjs +9 -0
- package/dist/_chunks/detect.mjs +29 -226
- package/dist/_chunks/detect.mjs.map +1 -1
- package/dist/_chunks/embedding-cache.mjs +0 -5
- package/dist/_chunks/embedding-cache2.mjs +1 -2
- package/dist/_chunks/formatting.mjs +0 -6
- package/dist/_chunks/formatting.mjs.map +1 -1
- package/dist/_chunks/index.d.mts +0 -10
- package/dist/_chunks/index.d.mts.map +1 -1
- package/dist/_chunks/index2.d.mts +3 -6
- package/dist/_chunks/index2.d.mts.map +1 -1
- package/dist/_chunks/index3.d.mts +2 -31
- package/dist/_chunks/index3.d.mts.map +1 -1
- package/dist/_chunks/install.mjs +0 -15
- package/dist/_chunks/install.mjs.map +1 -1
- package/dist/_chunks/libs/@sinclair/typebox.mjs +0 -444
- package/dist/_chunks/libs/@sinclair/typebox.mjs.map +1 -1
- package/dist/_chunks/list.mjs +0 -16
- package/dist/_chunks/list.mjs.map +1 -1
- package/dist/_chunks/lockfile.mjs +1 -10
- package/dist/_chunks/lockfile.mjs.map +1 -1
- package/dist/_chunks/markdown.mjs +0 -9
- package/dist/_chunks/markdown.mjs.map +1 -1
- package/dist/_chunks/package-json.mjs +0 -25
- package/dist/_chunks/package-json.mjs.map +1 -1
- package/dist/_chunks/pool2.mjs +0 -2
- package/dist/_chunks/pool2.mjs.map +1 -1
- package/dist/_chunks/prepare.mjs +8 -7
- package/dist/_chunks/prepare.mjs.map +1 -1
- package/dist/_chunks/prepare2.mjs +0 -18
- package/dist/_chunks/prepare2.mjs.map +1 -1
- package/dist/_chunks/prompts.mjs +1 -102
- package/dist/_chunks/prompts.mjs.map +1 -1
- package/dist/_chunks/retriv.mjs +23 -24
- package/dist/_chunks/retriv.mjs.map +1 -1
- package/dist/_chunks/rolldown-runtime.mjs +0 -2
- package/dist/_chunks/sanitize.mjs +0 -78
- package/dist/_chunks/sanitize.mjs.map +1 -1
- package/dist/_chunks/search-interactive.mjs +0 -17
- package/dist/_chunks/search-interactive.mjs.map +1 -1
- package/dist/_chunks/search.mjs +0 -19
- package/dist/_chunks/search2.mjs +3 -12
- package/dist/_chunks/search2.mjs.map +1 -1
- package/dist/_chunks/setup.mjs +0 -13
- package/dist/_chunks/setup.mjs.map +1 -1
- package/dist/_chunks/shared.mjs +0 -10
- package/dist/_chunks/shared.mjs.map +1 -1
- package/dist/_chunks/skills.mjs +2 -2
- package/dist/_chunks/skills.mjs.map +1 -1
- package/dist/_chunks/sources.mjs +3 -453
- package/dist/_chunks/sources.mjs.map +1 -1
- package/dist/_chunks/sync-shared.mjs +0 -16
- package/dist/_chunks/sync-shared2.mjs +0 -42
- package/dist/_chunks/sync-shared2.mjs.map +1 -1
- package/dist/_chunks/sync.mjs +1 -21
- package/dist/_chunks/sync.mjs.map +1 -1
- package/dist/_chunks/sync2.mjs +0 -20
- package/dist/_chunks/types.d.mts +0 -2
- package/dist/_chunks/types.d.mts.map +1 -1
- package/dist/_chunks/uninstall.mjs +0 -25
- package/dist/_chunks/uninstall.mjs.map +1 -1
- package/dist/_chunks/validate.mjs +0 -7
- package/dist/_chunks/validate.mjs.map +1 -1
- package/dist/_chunks/wizard.mjs +0 -2
- package/dist/_chunks/wizard.mjs.map +1 -1
- package/dist/_chunks/yaml.mjs +0 -21
- package/dist/_chunks/yaml.mjs.map +1 -1
- package/dist/agent/index.d.mts +0 -24
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +0 -8
- package/dist/cache/index.mjs +0 -2
- package/dist/cli-entry.mjs +0 -6
- package/dist/cli-entry.mjs.map +1 -1
- package/dist/cli.mjs +0 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +0 -6
- package/dist/prepare.mjs +0 -12
- package/dist/prepare.mjs.map +1 -1
- package/dist/retriv/index.mjs +0 -2
- package/dist/retriv/worker.d.mts +0 -3
- package/dist/retriv/worker.d.mts.map +1 -1
- package/dist/retriv/worker.mjs +0 -2
- package/dist/retriv/worker.mjs.map +1 -1
- package/dist/sources/index.mjs +0 -4
- package/package.json +17 -17
package/dist/_chunks/sources.mjs
CHANGED
|
@@ -7,19 +7,15 @@ import { tmpdir } from "node:os";
|
|
|
7
7
|
import { basename, dirname, join, resolve } from "pathe";
|
|
8
8
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
9
9
|
import { htmlToMarkdown } from "mdream";
|
|
10
|
+
import pLimit from "p-limit";
|
|
10
11
|
import { spawnSync } from "node:child_process";
|
|
11
12
|
import { ofetch } from "ofetch";
|
|
12
13
|
import { crawlAndGenerate } from "@mdream/crawl";
|
|
13
14
|
import { glob } from "tinyglobby";
|
|
14
15
|
import { downloadTemplate } from "giget";
|
|
15
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
-
import pLimit from "p-limit";
|
|
17
17
|
import { Writable } from "node:stream";
|
|
18
18
|
import { resolvePathSync } from "mlly";
|
|
19
|
-
//#region src/sources/github-common.ts
|
|
20
|
-
/**
|
|
21
|
-
* Shared constants and helpers for GitHub source modules (issues, discussions, releases)
|
|
22
|
-
*/
|
|
23
19
|
const BOT_USERS = new Set([
|
|
24
20
|
"renovate[bot]",
|
|
25
21
|
"dependabot[bot]",
|
|
@@ -27,25 +23,17 @@ const BOT_USERS = new Set([
|
|
|
27
23
|
"dependabot",
|
|
28
24
|
"github-actions[bot]"
|
|
29
25
|
]);
|
|
30
|
-
/** Extract YYYY-MM-DD date from an ISO timestamp */
|
|
31
26
|
const isoDate = (iso) => iso.split("T")[0];
|
|
32
|
-
/** Build YAML frontmatter from a key-value object, auto-quoting strings with special chars */
|
|
33
27
|
function buildFrontmatter(fields) {
|
|
34
28
|
const lines = ["---"];
|
|
35
29
|
for (const [k, v] of Object.entries(fields)) if (v !== void 0) lines.push(`${k}: ${typeof v === "string" ? yamlEscape(v) : v}`);
|
|
36
30
|
lines.push("---");
|
|
37
31
|
return lines.join("\n");
|
|
38
32
|
}
|
|
39
|
-
/** Check if body contains a code block */
|
|
40
33
|
function hasCodeBlock(text) {
|
|
41
34
|
return /```[\s\S]*?```/.test(text) || /`[^`]+`/.test(text);
|
|
42
35
|
}
|
|
43
|
-
/** Noise patterns in comments — filter these out */
|
|
44
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;
|
|
45
|
-
/**
|
|
46
|
-
* Smart body truncation — preserves code blocks and error messages.
|
|
47
|
-
* Instead of slicing at a char limit, finds a safe break point.
|
|
48
|
-
*/
|
|
49
37
|
function truncateBody(body, limit) {
|
|
50
38
|
if (body.length <= limit) return body;
|
|
51
39
|
const codeBlockRe = /```[\s\S]*?```/g;
|
|
@@ -66,10 +54,6 @@ function truncateBody(body, limit) {
|
|
|
66
54
|
return `${slice}...`;
|
|
67
55
|
}
|
|
68
56
|
let _ghToken;
|
|
69
|
-
/**
|
|
70
|
-
* Get GitHub auth token from gh CLI (cached).
|
|
71
|
-
* Returns null if gh CLI is not available or not authenticated.
|
|
72
|
-
*/
|
|
73
57
|
function getGitHubToken() {
|
|
74
58
|
if (_ghToken !== void 0) return _ghToken;
|
|
75
59
|
try {
|
|
@@ -88,13 +72,10 @@ function getGitHubToken() {
|
|
|
88
72
|
}
|
|
89
73
|
return _ghToken;
|
|
90
74
|
}
|
|
91
|
-
/** Repos where ungh.cc failed but gh api succeeded (likely private) */
|
|
92
75
|
const _needsAuth = /* @__PURE__ */ new Set();
|
|
93
|
-
/** Mark a repo as needing authenticated access */
|
|
94
76
|
function markRepoPrivate(owner, repo) {
|
|
95
77
|
_needsAuth.add(`${owner}/${repo}`);
|
|
96
78
|
}
|
|
97
|
-
/** Check if a repo is known to need authenticated access */
|
|
98
79
|
function isKnownPrivateRepo(owner, repo) {
|
|
99
80
|
return _needsAuth.has(`${owner}/${repo}`);
|
|
100
81
|
}
|
|
@@ -106,24 +87,15 @@ const ghApiFetch = ofetch.create({
|
|
|
106
87
|
headers: { "User-Agent": "skilld/1.0" }
|
|
107
88
|
});
|
|
108
89
|
const LINK_NEXT_RE = /<([^>]+)>;\s*rel="next"/;
|
|
109
|
-
/** Parse GitHub Link header for next page URL */
|
|
110
90
|
function parseLinkNext(header) {
|
|
111
91
|
if (!header) return null;
|
|
112
92
|
return header.match(LINK_NEXT_RE)?.[1] ?? null;
|
|
113
93
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Authenticated fetch against api.github.com. Returns null if no token or request fails.
|
|
116
|
-
* Endpoint should be relative, e.g. `repos/owner/repo/releases`.
|
|
117
|
-
*/
|
|
118
94
|
async function ghApi(endpoint) {
|
|
119
95
|
const token = getGitHubToken();
|
|
120
96
|
if (!token) return null;
|
|
121
97
|
return ghApiFetch(`${GH_API}/${endpoint}`, { headers: { Authorization: `token ${token}` } }).catch(() => null);
|
|
122
98
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Paginated GitHub API fetch. Follows Link headers, returns concatenated arrays.
|
|
125
|
-
* Endpoint should return a JSON array, e.g. `repos/owner/repo/releases`.
|
|
126
|
-
*/
|
|
127
99
|
async function ghApiPaginated(endpoint) {
|
|
128
100
|
const token = getGitHubToken();
|
|
129
101
|
if (!token) return [];
|
|
@@ -138,25 +110,16 @@ async function ghApiPaginated(endpoint) {
|
|
|
138
110
|
}
|
|
139
111
|
return results;
|
|
140
112
|
}
|
|
141
|
-
//#endregion
|
|
142
|
-
//#region src/sources/utils.ts
|
|
143
|
-
/**
|
|
144
|
-
* Shared utilities for doc resolution
|
|
145
|
-
*/
|
|
146
113
|
const $fetch = ofetch.create({
|
|
147
114
|
retry: 3,
|
|
148
115
|
retryDelay: 500,
|
|
149
116
|
timeout: 15e3,
|
|
150
117
|
headers: { "User-Agent": "skilld/1.0" }
|
|
151
118
|
});
|
|
152
|
-
/**
|
|
153
|
-
* Fetch text content from URL
|
|
154
|
-
*/
|
|
155
119
|
async function fetchText(url) {
|
|
156
120
|
return $fetch(url, { responseType: "text" }).catch(() => null);
|
|
157
121
|
}
|
|
158
122
|
const RAW_GH_RE = /raw\.githubusercontent\.com\/([^/]+)\/([^/]+)/;
|
|
159
|
-
/** Extract owner/repo from a GitHub raw content URL */
|
|
160
123
|
function extractGitHubRepo(url) {
|
|
161
124
|
const match = url.match(RAW_GH_RE);
|
|
162
125
|
return match ? {
|
|
@@ -164,14 +127,6 @@ function extractGitHubRepo(url) {
|
|
|
164
127
|
repo: match[2]
|
|
165
128
|
} : null;
|
|
166
129
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Fetch text from a GitHub raw URL with auth fallback for private repos.
|
|
169
|
-
* Tries unauthenticated first (fast path), falls back to authenticated
|
|
170
|
-
* request when the repo is known to be private or unauthenticated fails.
|
|
171
|
-
*
|
|
172
|
-
* Only sends auth tokens to raw.githubusercontent.com — returns null for
|
|
173
|
-
* non-GitHub URLs that fail unauthenticated to prevent token leakage.
|
|
174
|
-
*/
|
|
175
130
|
async function fetchGitHubRaw(url) {
|
|
176
131
|
const gh = extractGitHubRepo(url);
|
|
177
132
|
if (!(gh ? isKnownPrivateRepo(gh.owner, gh.repo) : false)) {
|
|
@@ -188,17 +143,11 @@ async function fetchGitHubRaw(url) {
|
|
|
188
143
|
if (content) markRepoPrivate(gh.owner, gh.repo);
|
|
189
144
|
return content;
|
|
190
145
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Verify URL exists and is not HTML (likely 404 page)
|
|
193
|
-
*/
|
|
194
146
|
async function verifyUrl(url) {
|
|
195
147
|
const res = await $fetch.raw(url, { method: "HEAD" }).catch(() => null);
|
|
196
148
|
if (!res) return false;
|
|
197
149
|
return !(res.headers.get("content-type") || "").includes("text/html");
|
|
198
150
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Check if URL points to a social media or package registry site (not real docs)
|
|
201
|
-
*/
|
|
202
151
|
const USELESS_HOSTS = new Set([
|
|
203
152
|
"twitter.com",
|
|
204
153
|
"x.com",
|
|
@@ -218,9 +167,6 @@ function isUselessDocsUrl(url) {
|
|
|
218
167
|
return false;
|
|
219
168
|
}
|
|
220
169
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Check if URL is a GitHub repo URL (not a docs site)
|
|
223
|
-
*/
|
|
224
170
|
function isGitHubRepoUrl(url) {
|
|
225
171
|
try {
|
|
226
172
|
const parsed = new URL(url);
|
|
@@ -229,9 +175,6 @@ function isGitHubRepoUrl(url) {
|
|
|
229
175
|
return false;
|
|
230
176
|
}
|
|
231
177
|
}
|
|
232
|
-
/**
|
|
233
|
-
* Parse owner/repo from GitHub URL
|
|
234
|
-
*/
|
|
235
178
|
function parseGitHubUrl(url) {
|
|
236
179
|
const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[/#]|$)/);
|
|
237
180
|
if (!match) return null;
|
|
@@ -240,16 +183,9 @@ function parseGitHubUrl(url) {
|
|
|
240
183
|
repo: match[2]
|
|
241
184
|
};
|
|
242
185
|
}
|
|
243
|
-
/**
|
|
244
|
-
* Normalize git repo URL to https
|
|
245
|
-
*/
|
|
246
186
|
function normalizeRepoUrl(url) {
|
|
247
187
|
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/");
|
|
248
188
|
}
|
|
249
|
-
/**
|
|
250
|
-
* Parse package spec with optional dist-tag or version: "vue@beta" → { name: "vue", tag: "beta" }
|
|
251
|
-
* Handles scoped packages: "@vue/reactivity@beta" → { name: "@vue/reactivity", tag: "beta" }
|
|
252
|
-
*/
|
|
253
189
|
function parsePackageSpec(spec) {
|
|
254
190
|
if (spec.startsWith("@")) {
|
|
255
191
|
const slashIdx = spec.indexOf("/");
|
|
@@ -269,9 +205,6 @@ function parsePackageSpec(spec) {
|
|
|
269
205
|
};
|
|
270
206
|
return { name: spec };
|
|
271
207
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Extract branch hint from URL fragment (e.g. "git+https://...#main" → "main")
|
|
274
|
-
*/
|
|
275
208
|
function extractBranchHint(url) {
|
|
276
209
|
const hash = url.indexOf("#");
|
|
277
210
|
if (hash === -1) return void 0;
|
|
@@ -279,11 +212,6 @@ function extractBranchHint(url) {
|
|
|
279
212
|
if (!fragment || fragment === "readme") return void 0;
|
|
280
213
|
return fragment;
|
|
281
214
|
}
|
|
282
|
-
//#endregion
|
|
283
|
-
//#region src/sources/releases.ts
|
|
284
|
-
/**
|
|
285
|
-
* GitHub release notes fetching via GitHub API (preferred) with ungh.cc fallback
|
|
286
|
-
*/
|
|
287
215
|
function parseSemver(version) {
|
|
288
216
|
const clean = version.replace(/^v/, "");
|
|
289
217
|
const match = clean.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
@@ -295,13 +223,6 @@ function parseSemver(version) {
|
|
|
295
223
|
raw: clean
|
|
296
224
|
};
|
|
297
225
|
}
|
|
298
|
-
/**
|
|
299
|
-
* Extract version from a release tag, handling monorepo formats:
|
|
300
|
-
* - `pkg@1.2.3` → `1.2.3`
|
|
301
|
-
* - `pkg-v1.2.3` → `1.2.3`
|
|
302
|
-
* - `v1.2.3` → `1.2.3`
|
|
303
|
-
* - `1.2.3` → `1.2.3`
|
|
304
|
-
*/
|
|
305
226
|
function extractVersion(tag, packageName) {
|
|
306
227
|
if (packageName) {
|
|
307
228
|
const atMatch = tag.match(new RegExp(`^${escapeRegex(packageName)}@(.+)$`));
|
|
@@ -314,15 +235,9 @@ function extractVersion(tag, packageName) {
|
|
|
314
235
|
function escapeRegex(str) {
|
|
315
236
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
316
237
|
}
|
|
317
|
-
/**
|
|
318
|
-
* Check if a release tag belongs to a specific package
|
|
319
|
-
*/
|
|
320
238
|
function tagMatchesPackage(tag, packageName) {
|
|
321
239
|
return tag.startsWith(`${packageName}@`) || tag.startsWith(`${packageName}-v`) || tag.startsWith(`${packageName}-`);
|
|
322
240
|
}
|
|
323
|
-
/**
|
|
324
|
-
* Check if a version string contains a prerelease suffix (e.g. 6.0.0-beta, 1.2.3-rc.1)
|
|
325
|
-
*/
|
|
326
241
|
function isPrerelease(version) {
|
|
327
242
|
return /^\d+\.\d+\.\d+-.+/.test(version.replace(/^v/, ""));
|
|
328
243
|
}
|
|
@@ -331,7 +246,6 @@ function compareSemver(a, b) {
|
|
|
331
246
|
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
332
247
|
return a.patch - b.patch;
|
|
333
248
|
}
|
|
334
|
-
/** Map GitHub API release to our GitHubRelease shape */
|
|
335
249
|
function mapApiRelease(r) {
|
|
336
250
|
return {
|
|
337
251
|
id: r.id,
|
|
@@ -343,20 +257,11 @@ function mapApiRelease(r) {
|
|
|
343
257
|
markdown: r.body
|
|
344
258
|
};
|
|
345
259
|
}
|
|
346
|
-
/**
|
|
347
|
-
* Fetch all releases — GitHub API first (authenticated, async), ungh.cc fallback
|
|
348
|
-
*/
|
|
349
260
|
async function fetchAllReleases(owner, repo) {
|
|
350
261
|
const apiReleases = await ghApiPaginated(`repos/${owner}/${repo}/releases`);
|
|
351
262
|
if (apiReleases.length > 0) return apiReleases.map(mapApiRelease);
|
|
352
263
|
return (await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`, { signal: AbortSignal.timeout(15e3) }).catch(() => null))?.releases ?? [];
|
|
353
264
|
}
|
|
354
|
-
/**
|
|
355
|
-
* Select last 20 stable releases for a package, sorted newest first.
|
|
356
|
-
* For monorepos, filters to package-specific tags (pkg@version).
|
|
357
|
-
* Falls back to generic tags (v1.2.3) only if no package-specific found.
|
|
358
|
-
* If installedVersion is provided, filters out releases newer than it.
|
|
359
|
-
*/
|
|
360
265
|
function selectReleases(releases, packageName, installedVersion, fromDate) {
|
|
361
266
|
const hasMonorepoTags = packageName && releases.some((r) => tagMatchesPackage(r.tag, packageName));
|
|
362
267
|
const installedSv = installedVersion ? parseSemver(installedVersion) : null;
|
|
@@ -386,9 +291,6 @@ function selectReleases(releases, packageName, installedVersion, fromDate) {
|
|
|
386
291
|
});
|
|
387
292
|
return fromDate ? sorted : sorted.slice(0, 20);
|
|
388
293
|
}
|
|
389
|
-
/**
|
|
390
|
-
* Format a release as markdown with YAML frontmatter
|
|
391
|
-
*/
|
|
392
294
|
function formatRelease(release, packageName) {
|
|
393
295
|
const date = isoDate(release.publishedAt || release.createdAt);
|
|
394
296
|
const version = extractVersion(release.tag, packageName) || release.tag;
|
|
@@ -402,10 +304,6 @@ function formatRelease(release, packageName) {
|
|
|
402
304
|
fm.push("---");
|
|
403
305
|
return `${fm.join("\n")}\n\n# ${release.name || release.tag}\n\n${release.markdown}`;
|
|
404
306
|
}
|
|
405
|
-
/**
|
|
406
|
-
* Generate a unified summary index of all releases for quick LLM scanning.
|
|
407
|
-
* Includes GitHub releases, blog release posts, and CHANGELOG link.
|
|
408
|
-
*/
|
|
409
307
|
function generateReleaseIndex(releasesOrOpts, packageName) {
|
|
410
308
|
const opts = Array.isArray(releasesOrOpts) ? {
|
|
411
309
|
releases: releasesOrOpts,
|
|
@@ -447,18 +345,10 @@ function generateReleaseIndex(releasesOrOpts, packageName) {
|
|
|
447
345
|
}
|
|
448
346
|
return lines.join("\n");
|
|
449
347
|
}
|
|
450
|
-
/**
|
|
451
|
-
* Check if a single release is a stub redirecting to CHANGELOG.md.
|
|
452
|
-
* Short body (<500 chars) that mentions CHANGELOG indicates no real content.
|
|
453
|
-
*/
|
|
454
348
|
function isStubRelease(release) {
|
|
455
349
|
const body = (release.markdown || "").trim();
|
|
456
350
|
return body.length < 500 && /changelog\.md/i.test(body);
|
|
457
351
|
}
|
|
458
|
-
/**
|
|
459
|
-
* Fetch CHANGELOG.md from a GitHub repo at a specific ref as fallback.
|
|
460
|
-
* For monorepos, also checks packages/{shortName}/CHANGELOG.md.
|
|
461
|
-
*/
|
|
462
352
|
async function fetchChangelog(owner, repo, ref, packageName) {
|
|
463
353
|
const paths = [];
|
|
464
354
|
if (packageName) {
|
|
@@ -474,13 +364,6 @@ async function fetchChangelog(owner, repo, ref, packageName) {
|
|
|
474
364
|
}
|
|
475
365
|
return null;
|
|
476
366
|
}
|
|
477
|
-
/**
|
|
478
|
-
* Fetch release notes for a package. Returns CachedDoc[] with releases/{tag}.md files.
|
|
479
|
-
*
|
|
480
|
-
* Strategy:
|
|
481
|
-
* 1. Fetch GitHub releases, filter to package-specific tags for monorepos
|
|
482
|
-
* 2. If no releases found, try CHANGELOG.md as fallback
|
|
483
|
-
*/
|
|
484
367
|
async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageName, fromDate, changelogRef) {
|
|
485
368
|
const selected = selectReleases(await fetchAllReleases(owner, repo), packageName, installedVersion, fromDate);
|
|
486
369
|
if (selected.length > 0) {
|
|
@@ -504,11 +387,6 @@ async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageN
|
|
|
504
387
|
content: changelog
|
|
505
388
|
}];
|
|
506
389
|
}
|
|
507
|
-
//#endregion
|
|
508
|
-
//#region src/sources/blog-releases.ts
|
|
509
|
-
/**
|
|
510
|
-
* Format a blog release as markdown with YAML frontmatter
|
|
511
|
-
*/
|
|
512
390
|
function formatBlogRelease(release) {
|
|
513
391
|
return `${[
|
|
514
392
|
"---",
|
|
@@ -520,9 +398,6 @@ function formatBlogRelease(release) {
|
|
|
520
398
|
"---"
|
|
521
399
|
].join("\n")}\n\n# ${release.title}\n\n${release.markdown}`;
|
|
522
400
|
}
|
|
523
|
-
/**
|
|
524
|
-
* Fetch and parse a single blog post using preset metadata for version/date
|
|
525
|
-
*/
|
|
526
401
|
async function fetchBlogPost(entry) {
|
|
527
402
|
try {
|
|
528
403
|
const html = await $fetch(entry.url, {
|
|
@@ -550,11 +425,6 @@ async function fetchBlogPost(entry) {
|
|
|
550
425
|
return null;
|
|
551
426
|
}
|
|
552
427
|
}
|
|
553
|
-
/**
|
|
554
|
-
* Filter blog releases by installed version
|
|
555
|
-
* Only includes releases where version <= installedVersion
|
|
556
|
-
* Returns all releases if version parsing fails (fail-safe)
|
|
557
|
-
*/
|
|
558
428
|
function filterBlogsByVersion(entries, installedVersion) {
|
|
559
429
|
const installedSv = parseSemver(installedVersion);
|
|
560
430
|
if (!installedSv) return entries;
|
|
@@ -564,23 +434,13 @@ function filterBlogsByVersion(entries, installedVersion) {
|
|
|
564
434
|
return compareSemver(entrySv, installedSv) <= 0;
|
|
565
435
|
});
|
|
566
436
|
}
|
|
567
|
-
/**
|
|
568
|
-
* Fetch blog release notes from package presets
|
|
569
|
-
* Filters to only releases matching or older than the installed version
|
|
570
|
-
* Returns CachedDoc[] with releases/blog-{version}.md files
|
|
571
|
-
*/
|
|
572
437
|
async function fetchBlogReleases(packageName, installedVersion) {
|
|
573
438
|
const preset = getBlogPreset(packageName);
|
|
574
439
|
if (!preset) return [];
|
|
575
440
|
const filteredReleases = filterBlogsByVersion(preset.releases, installedVersion);
|
|
576
441
|
if (filteredReleases.length === 0) return [];
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
for (let i = 0; i < filteredReleases.length; i += batchSize) {
|
|
580
|
-
const batch = filteredReleases.slice(i, i + batchSize);
|
|
581
|
-
const results = await Promise.all(batch.map((entry) => fetchBlogPost(entry)));
|
|
582
|
-
for (const result of results) if (result) releases.push(result);
|
|
583
|
-
}
|
|
442
|
+
const limit = pLimit(3);
|
|
443
|
+
const releases = (await Promise.all(filteredReleases.map((entry) => limit(() => fetchBlogPost(entry))))).filter((r) => r !== null);
|
|
584
444
|
if (releases.length === 0) return [];
|
|
585
445
|
releases.sort((a, b) => {
|
|
586
446
|
const aVer = a.version.split(".").map(Number);
|
|
@@ -596,19 +456,6 @@ async function fetchBlogReleases(packageName, installedVersion) {
|
|
|
596
456
|
content: formatBlogRelease(r)
|
|
597
457
|
}));
|
|
598
458
|
}
|
|
599
|
-
//#endregion
|
|
600
|
-
//#region src/sources/crawl.ts
|
|
601
|
-
/**
|
|
602
|
-
* Website crawl doc source — fetches docs by crawling a URL pattern
|
|
603
|
-
*/
|
|
604
|
-
/**
|
|
605
|
-
* Crawl a URL pattern and return docs as cached doc format.
|
|
606
|
-
* Uses HTTP crawler (no browser needed) with sitemap discovery + glob filtering.
|
|
607
|
-
*
|
|
608
|
-
* @param url - URL with optional glob pattern (e.g. 'https://example.com/docs/**')
|
|
609
|
-
* @param onProgress - Optional progress callback
|
|
610
|
-
* @param maxPages - Max pages to crawl (default 200)
|
|
611
|
-
*/
|
|
612
459
|
async function fetchCrawledDocs(url, onProgress, maxPages = 200) {
|
|
613
460
|
const outputDir = join(tmpdir(), "skilld-crawl", Date.now().toString());
|
|
614
461
|
onProgress?.(`Crawling ${url}`);
|
|
@@ -664,11 +511,9 @@ async function fetchCrawledDocs(url, onProgress, maxPages = 200) {
|
|
|
664
511
|
return docs;
|
|
665
512
|
}
|
|
666
513
|
const HTML_LANG_RE = /<html[^>]*\slang=["']([^"']+)["']/i;
|
|
667
|
-
/** Extract lang attribute from <html> tag */
|
|
668
514
|
function extractHtmlLang(html) {
|
|
669
515
|
return HTML_LANG_RE.exec(html)?.[1]?.toLowerCase();
|
|
670
516
|
}
|
|
671
|
-
/** Common ISO 639-1 locale codes for i18n'd doc sites */
|
|
672
517
|
const LOCALE_CODES = new Set([
|
|
673
518
|
"ar",
|
|
674
519
|
"de",
|
|
@@ -691,39 +536,25 @@ const LOCALE_CODES = new Set([
|
|
|
691
536
|
"zh-cn",
|
|
692
537
|
"zh-tw"
|
|
693
538
|
]);
|
|
694
|
-
/** Check if a URL path segment is a known locale prefix foreign to both English and user's locale */
|
|
695
539
|
function isForeignPathPrefix(segment, userLang) {
|
|
696
540
|
if (!segment) return false;
|
|
697
541
|
const lower = segment.toLowerCase();
|
|
698
542
|
if (lower === "en" || lower.startsWith(userLang)) return false;
|
|
699
543
|
return LOCALE_CODES.has(lower);
|
|
700
544
|
}
|
|
701
|
-
/** Detect user's 2-letter language code from env (e.g. 'ja' from LANG=ja_JP.UTF-8) */
|
|
702
545
|
function getUserLang() {
|
|
703
546
|
const code = (process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || "").split(/[_.:-]/)[0]?.toLowerCase() || "";
|
|
704
547
|
return code.length >= 2 ? code.slice(0, 2) : "en";
|
|
705
548
|
}
|
|
706
|
-
/** Append glob pattern to a docs URL for crawling */
|
|
707
549
|
function toCrawlPattern(docsUrl) {
|
|
708
550
|
return `${docsUrl.replace(/\/+$/, "")}/**`;
|
|
709
551
|
}
|
|
710
|
-
//#endregion
|
|
711
|
-
//#region src/sources/issues.ts
|
|
712
|
-
/**
|
|
713
|
-
* GitHub issues fetching via gh CLI Search API
|
|
714
|
-
* Freshness-weighted scoring, type quotas, comment quality filtering
|
|
715
|
-
* Categorized by labels, noise filtered out, non-technical issues detected
|
|
716
|
-
*/
|
|
717
552
|
let _ghAvailable;
|
|
718
|
-
/**
|
|
719
|
-
* Check if gh CLI is installed and authenticated (cached)
|
|
720
|
-
*/
|
|
721
553
|
function isGhAvailable() {
|
|
722
554
|
if (_ghAvailable !== void 0) return _ghAvailable;
|
|
723
555
|
const { status } = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
|
|
724
556
|
return _ghAvailable = status === 0;
|
|
725
557
|
}
|
|
726
|
-
/** Labels that indicate noise — filter these out entirely */
|
|
727
558
|
const NOISE_LABELS = new Set([
|
|
728
559
|
"duplicate",
|
|
729
560
|
"stale",
|
|
@@ -735,7 +566,6 @@ const NOISE_LABELS = new Set([
|
|
|
735
566
|
"needs triage",
|
|
736
567
|
"triage"
|
|
737
568
|
]);
|
|
738
|
-
/** Labels that indicate feature requests — deprioritize */
|
|
739
569
|
const FEATURE_LABELS = new Set([
|
|
740
570
|
"enhancement",
|
|
741
571
|
"feature",
|
|
@@ -771,7 +601,6 @@ const DOCS_LABELS = new Set([
|
|
|
771
601
|
"doc",
|
|
772
602
|
"typo"
|
|
773
603
|
]);
|
|
774
|
-
/** Cache compiled word-boundary regexes per keyword set */
|
|
775
604
|
const labelRegexCache = /* @__PURE__ */ new WeakMap();
|
|
776
605
|
function getLabelRegex(keywords) {
|
|
777
606
|
let re = labelRegexCache.get(keywords);
|
|
@@ -782,18 +611,10 @@ function getLabelRegex(keywords) {
|
|
|
782
611
|
}
|
|
783
612
|
return re;
|
|
784
613
|
}
|
|
785
|
-
/**
|
|
786
|
-
* Check if a label matches any keyword from a set using word boundaries.
|
|
787
|
-
* Handles emoji-prefixed labels like ":sparkles: feature request" or ":lady_beetle: bug"
|
|
788
|
-
* without false positives on substrings (e.g. "debug" should not match "bug").
|
|
789
|
-
*/
|
|
790
614
|
function labelMatchesAny(label, keywords) {
|
|
791
615
|
if (keywords.has(label)) return true;
|
|
792
616
|
return getLabelRegex(keywords).test(label);
|
|
793
617
|
}
|
|
794
|
-
/**
|
|
795
|
-
* Classify an issue by its labels into a type useful for skill generation
|
|
796
|
-
*/
|
|
797
618
|
function classifyIssue(labels) {
|
|
798
619
|
const lower = labels.map((l) => l.toLowerCase());
|
|
799
620
|
if (lower.some((l) => labelMatchesAny(l, BUG_LABELS))) return "bug";
|
|
@@ -802,37 +623,20 @@ function classifyIssue(labels) {
|
|
|
802
623
|
if (lower.some((l) => labelMatchesAny(l, FEATURE_LABELS))) return "feature";
|
|
803
624
|
return "other";
|
|
804
625
|
}
|
|
805
|
-
/**
|
|
806
|
-
* Check if an issue should be filtered out entirely
|
|
807
|
-
*/
|
|
808
626
|
function isNoiseIssue(issue) {
|
|
809
627
|
if (issue.labels.map((l) => l.toLowerCase()).some((l) => labelMatchesAny(l, NOISE_LABELS))) return true;
|
|
810
628
|
if (issue.title.startsWith("☂️") || issue.title.startsWith("[META]") || issue.title.startsWith("[Tracking]")) return true;
|
|
811
629
|
return false;
|
|
812
630
|
}
|
|
813
|
-
/**
|
|
814
|
-
* Detect non-technical issues: fan mail, showcases, sentiment.
|
|
815
|
-
* Short body + no code + high reactions = likely non-technical.
|
|
816
|
-
* Note: roadmap/tracking issues are NOT filtered — they get score-boosted instead.
|
|
817
|
-
*/
|
|
818
631
|
function isNonTechnical(issue) {
|
|
819
632
|
const body = (issue.body || "").trim();
|
|
820
633
|
if (body.length < 200 && !hasCodeBlock(body) && issue.reactions > 50) return true;
|
|
821
634
|
if (/\b(?:love|thank|awesome|great work)\b/i.test(issue.title) && !hasCodeBlock(body)) return true;
|
|
822
635
|
return false;
|
|
823
636
|
}
|
|
824
|
-
/**
|
|
825
|
-
* Freshness-weighted score: reactions * decay(age_in_years)
|
|
826
|
-
* Steep decay so recent issues dominate over old high-reaction ones.
|
|
827
|
-
* At 0.6: 1yr=0.63x, 2yr=0.45x, 4yr=0.29x, 6yr=0.22x
|
|
828
|
-
*/
|
|
829
637
|
function freshnessScore(reactions, createdAt) {
|
|
830
638
|
return reactions * (1 / (1 + (Date.now() - new Date(createdAt).getTime()) / (365.25 * 24 * 60 * 60 * 1e3) * .6));
|
|
831
639
|
}
|
|
832
|
-
/**
|
|
833
|
-
* Type quotas — guarantee a mix of issue types.
|
|
834
|
-
* Bugs and questions get priority; feature requests are hard-capped.
|
|
835
|
-
*/
|
|
836
640
|
function applyTypeQuotas(issues, limit) {
|
|
837
641
|
const byType = /* @__PURE__ */ new Map();
|
|
838
642
|
for (const issue of issues) mapInsert(byType, issue.type, () => []).push(issue);
|
|
@@ -866,17 +670,11 @@ function applyTypeQuotas(issues, limit) {
|
|
|
866
670
|
}
|
|
867
671
|
return selected.sort((a, b) => b.score - a.score);
|
|
868
672
|
}
|
|
869
|
-
/**
|
|
870
|
-
* Body truncation limit based on reactions — high-reaction issues deserve more space
|
|
871
|
-
*/
|
|
872
673
|
function bodyLimit(reactions) {
|
|
873
674
|
if (reactions >= 10) return 2e3;
|
|
874
675
|
if (reactions >= 5) return 1500;
|
|
875
676
|
return 800;
|
|
876
677
|
}
|
|
877
|
-
/**
|
|
878
|
-
* Fetch issues for a state using GitHub Search API sorted by reactions
|
|
879
|
-
*/
|
|
880
678
|
function fetchIssuesByState(owner, repo, state, count, releasedAt, fromDate) {
|
|
881
679
|
const fetchCount = Math.min(count * 3, 100);
|
|
882
680
|
let datePart = "";
|
|
@@ -922,12 +720,6 @@ function oneYearAgo() {
|
|
|
922
720
|
d.setFullYear(d.getFullYear() - 1);
|
|
923
721
|
return isoDate(d.toISOString());
|
|
924
722
|
}
|
|
925
|
-
/**
|
|
926
|
-
* Batch-fetch top comments for issues via GraphQL.
|
|
927
|
-
* Enriches the top N highest-score issues with their best comments.
|
|
928
|
-
* Prioritizes: comments with code blocks, from maintainers, with high reactions.
|
|
929
|
-
* Filters out "+1", "any updates?", "same here" noise.
|
|
930
|
-
*/
|
|
931
723
|
function enrichWithComments(owner, repo, issues, topN = 15) {
|
|
932
724
|
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);
|
|
933
725
|
if (worth.length === 0) return;
|
|
@@ -975,10 +767,6 @@ function enrichWithComments(owner, repo, issues, topN = 15) {
|
|
|
975
767
|
}
|
|
976
768
|
} catch {}
|
|
977
769
|
}
|
|
978
|
-
/**
|
|
979
|
-
* Try to detect which version fixed a closed issue from maintainer comments.
|
|
980
|
-
* Looks for version patterns in maintainer/collaborator comments.
|
|
981
|
-
*/
|
|
982
770
|
function detectResolvedVersion(comments) {
|
|
983
771
|
const maintainerComments = comments.filter((c) => c.isMaintainer);
|
|
984
772
|
for (const c of maintainerComments.reverse()) {
|
|
@@ -990,11 +778,6 @@ function detectResolvedVersion(comments) {
|
|
|
990
778
|
}
|
|
991
779
|
}
|
|
992
780
|
}
|
|
993
|
-
/**
|
|
994
|
-
* Fetch issues from a GitHub repo with freshness-weighted scoring and type quotas.
|
|
995
|
-
* Returns a balanced mix: bugs > questions > docs > other > features.
|
|
996
|
-
* Filters noise, non-technical content, and enriches with quality comments.
|
|
997
|
-
*/
|
|
998
781
|
async function fetchGitHubIssues(owner, repo, limit = 30, releasedAt, fromDate) {
|
|
999
782
|
if (!isGhAvailable()) return [];
|
|
1000
783
|
const openCount = Math.ceil(limit * .75);
|
|
@@ -1009,9 +792,6 @@ async function fetchGitHubIssues(owner, repo, limit = 30, releasedAt, fromDate)
|
|
|
1009
792
|
return [];
|
|
1010
793
|
}
|
|
1011
794
|
}
|
|
1012
|
-
/**
|
|
1013
|
-
* Format a single issue as markdown with YAML frontmatter
|
|
1014
|
-
*/
|
|
1015
795
|
function formatIssueAsMarkdown(issue) {
|
|
1016
796
|
const limit = bodyLimit(issue.reactions);
|
|
1017
797
|
const fmFields = {
|
|
@@ -1046,10 +826,6 @@ function formatIssueAsMarkdown(issue) {
|
|
|
1046
826
|
}
|
|
1047
827
|
return lines.join("\n");
|
|
1048
828
|
}
|
|
1049
|
-
/**
|
|
1050
|
-
* Generate a summary index of all issues for quick LLM scanning.
|
|
1051
|
-
* Groups by type so the LLM can quickly find bugs vs questions.
|
|
1052
|
-
*/
|
|
1053
829
|
function generateIssueIndex(issues) {
|
|
1054
830
|
const byType = /* @__PURE__ */ new Map();
|
|
1055
831
|
for (const issue of issues) mapInsert(byType, issue.type, () => []).push(issue);
|
|
@@ -1094,14 +870,6 @@ function generateIssueIndex(issues) {
|
|
|
1094
870
|
}
|
|
1095
871
|
return sections.join("\n");
|
|
1096
872
|
}
|
|
1097
|
-
//#endregion
|
|
1098
|
-
//#region src/sources/discussions.ts
|
|
1099
|
-
/**
|
|
1100
|
-
* GitHub discussions fetching via gh CLI GraphQL
|
|
1101
|
-
* Prioritizes Q&A and Help categories, includes accepted answers
|
|
1102
|
-
* Comment quality filtering, smart truncation, noise removal
|
|
1103
|
-
*/
|
|
1104
|
-
/** Categories most useful for skill generation (in priority order) */
|
|
1105
873
|
const HIGH_VALUE_CATEGORIES = new Set([
|
|
1106
874
|
"q&a",
|
|
1107
875
|
"help",
|
|
@@ -1113,21 +881,11 @@ const LOW_VALUE_CATEGORIES = new Set([
|
|
|
1113
881
|
"ideas",
|
|
1114
882
|
"polls"
|
|
1115
883
|
]);
|
|
1116
|
-
/** Off-topic or spam title patterns — instant reject */
|
|
1117
884
|
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;
|
|
1118
|
-
/** Minimum score for a discussion to be included */
|
|
1119
885
|
const MIN_DISCUSSION_SCORE = 3;
|
|
1120
|
-
/**
|
|
1121
|
-
* Score a comment for quality. Higher = more useful for skill generation.
|
|
1122
|
-
* Maintainers 3x, code blocks 2x, reactions linear.
|
|
1123
|
-
*/
|
|
1124
886
|
function scoreComment(c) {
|
|
1125
887
|
return (c.isMaintainer ? 3 : 1) * (hasCodeBlock(c.body) ? 2 : 1) * (1 + c.reactions);
|
|
1126
888
|
}
|
|
1127
|
-
/**
|
|
1128
|
-
* Score a discussion for overall quality. Used for filtering and sorting.
|
|
1129
|
-
* Returns -1 for instant-reject (spam/off-topic).
|
|
1130
|
-
*/
|
|
1131
889
|
function scoreDiscussion(d) {
|
|
1132
890
|
if (TITLE_NOISE_RE.test(d.title)) return -1;
|
|
1133
891
|
let score = 0;
|
|
@@ -1146,11 +904,6 @@ function scoreDiscussion(d) {
|
|
|
1146
904
|
if (d.topComments.some((c) => c.reactions > 0)) score += 1;
|
|
1147
905
|
return score;
|
|
1148
906
|
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Fetch discussions from a GitHub repo using gh CLI GraphQL.
|
|
1151
|
-
* Prioritizes Q&A and Help categories. Includes accepted answer body for answered discussions.
|
|
1152
|
-
* Fetches extra comments and scores them for quality.
|
|
1153
|
-
*/
|
|
1154
907
|
async function fetchGitHubDiscussions(owner, repo, limit = 20, releasedAt, fromDate) {
|
|
1155
908
|
if (!isGhAvailable()) return [];
|
|
1156
909
|
if (!fromDate && releasedAt) {
|
|
@@ -1233,9 +986,6 @@ async function fetchGitHubDiscussions(owner, repo, limit = 20, releasedAt, fromD
|
|
|
1233
986
|
return [];
|
|
1234
987
|
}
|
|
1235
988
|
}
|
|
1236
|
-
/**
|
|
1237
|
-
* Format a single discussion as markdown with YAML frontmatter
|
|
1238
|
-
*/
|
|
1239
989
|
function formatDiscussionAsMarkdown(d) {
|
|
1240
990
|
const fm = buildFrontmatter({
|
|
1241
991
|
number: d.number,
|
|
@@ -1265,10 +1015,6 @@ function formatDiscussionAsMarkdown(d) {
|
|
|
1265
1015
|
}
|
|
1266
1016
|
return lines.join("\n");
|
|
1267
1017
|
}
|
|
1268
|
-
/**
|
|
1269
|
-
* Generate a summary index of all discussions for quick LLM scanning.
|
|
1270
|
-
* Groups by category so the LLM can quickly find Q&A vs general discussions.
|
|
1271
|
-
*/
|
|
1272
1018
|
function generateDiscussionIndex(discussions) {
|
|
1273
1019
|
const byCategory = /* @__PURE__ */ new Map();
|
|
1274
1020
|
for (const d of discussions) mapInsert(byCategory, d.category || "Uncategorized", () => []).push(d);
|
|
@@ -1300,16 +1046,6 @@ function generateDiscussionIndex(discussions) {
|
|
|
1300
1046
|
}
|
|
1301
1047
|
return sections.join("\n");
|
|
1302
1048
|
}
|
|
1303
|
-
//#endregion
|
|
1304
|
-
//#region src/sources/docs.ts
|
|
1305
|
-
/**
|
|
1306
|
-
* Docs index generation — creates _INDEX.md for docs directory
|
|
1307
|
-
*/
|
|
1308
|
-
/**
|
|
1309
|
-
* Generate a _INDEX.md for a docs/ directory.
|
|
1310
|
-
* Input: array of cached docs with paths like `docs/api/reactivity.md`.
|
|
1311
|
-
* Output: markdown index grouped by directory with title + description per page.
|
|
1312
|
-
*/
|
|
1313
1049
|
function generateDocsIndex(docs) {
|
|
1314
1050
|
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));
|
|
1315
1051
|
if (docFiles.length === 0) return "";
|
|
@@ -1354,12 +1090,6 @@ function generateDocsIndex(docs) {
|
|
|
1354
1090
|
}
|
|
1355
1091
|
return sections.join("\n");
|
|
1356
1092
|
}
|
|
1357
|
-
//#endregion
|
|
1358
|
-
//#region src/sources/entries.ts
|
|
1359
|
-
/**
|
|
1360
|
-
* Globs .d.ts type definition files from a package for search indexing.
|
|
1361
|
-
* Only types — source code is too verbose.
|
|
1362
|
-
*/
|
|
1363
1093
|
const SKIP_DIRS = [
|
|
1364
1094
|
"node_modules",
|
|
1365
1095
|
"_vendor",
|
|
@@ -1387,9 +1117,6 @@ const SKIP_PATTERNS = [
|
|
|
1387
1117
|
"README*"
|
|
1388
1118
|
];
|
|
1389
1119
|
const MAX_FILE_SIZE = 500 * 1024;
|
|
1390
|
-
/**
|
|
1391
|
-
* Glob .d.ts type definition files from a package directory, skipping junk.
|
|
1392
|
-
*/
|
|
1393
1120
|
async function resolveEntryFiles(packageDir) {
|
|
1394
1121
|
if (!existsSync(join(packageDir, "package.json"))) return [];
|
|
1395
1122
|
const files = await glob(["**/*.d.{ts,mts,cts}"], {
|
|
@@ -1416,18 +1143,6 @@ async function resolveEntryFiles(packageDir) {
|
|
|
1416
1143
|
}
|
|
1417
1144
|
return entries;
|
|
1418
1145
|
}
|
|
1419
|
-
//#endregion
|
|
1420
|
-
//#region src/sources/git-skills.ts
|
|
1421
|
-
/**
|
|
1422
|
-
* Git repo skill source — parse inputs + fetch pre-authored skills from repos
|
|
1423
|
-
*
|
|
1424
|
-
* Supports GitHub shorthand (owner/repo), full URLs, SSH, GitLab, and local paths.
|
|
1425
|
-
* Skills are pre-authored SKILL.md files — no doc resolution or LLM generation needed.
|
|
1426
|
-
*/
|
|
1427
|
-
/**
|
|
1428
|
-
* Detect whether an input string is a git skill source.
|
|
1429
|
-
* Returns null for npm package names (including scoped @scope/pkg).
|
|
1430
|
-
*/
|
|
1431
1146
|
function parseGitSkillInput(input) {
|
|
1432
1147
|
const trimmed = input.trim();
|
|
1433
1148
|
if (trimmed.startsWith("@")) return null;
|
|
@@ -1489,9 +1204,6 @@ function parseGitUrl(url) {
|
|
|
1489
1204
|
return null;
|
|
1490
1205
|
}
|
|
1491
1206
|
}
|
|
1492
|
-
/**
|
|
1493
|
-
* Parse name and description from SKILL.md frontmatter.
|
|
1494
|
-
*/
|
|
1495
1207
|
function parseSkillFrontmatterName(content) {
|
|
1496
1208
|
const fm = parseFrontmatter(content);
|
|
1497
1209
|
return {
|
|
@@ -1499,7 +1211,6 @@ function parseSkillFrontmatterName(content) {
|
|
|
1499
1211
|
description: fm.description
|
|
1500
1212
|
};
|
|
1501
1213
|
}
|
|
1502
|
-
/** Recursively collect all files in a directory, returning relative paths */
|
|
1503
1214
|
function collectFiles(dir, prefix = "") {
|
|
1504
1215
|
const files = [];
|
|
1505
1216
|
if (!existsSync(dir)) return files;
|
|
@@ -1514,9 +1225,6 @@ function collectFiles(dir, prefix = "") {
|
|
|
1514
1225
|
}
|
|
1515
1226
|
return files;
|
|
1516
1227
|
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Fetch skills from a git source. Returns list of discovered skills.
|
|
1519
|
-
*/
|
|
1520
1228
|
async function fetchGitSkills(source, onProgress) {
|
|
1521
1229
|
if (source.type === "local") return fetchLocalSkills(source);
|
|
1522
1230
|
if (source.type === "github") return fetchGitHubSkills(source, onProgress);
|
|
@@ -1666,19 +1374,11 @@ async function fetchGitLabSkills(source, onProgress) {
|
|
|
1666
1374
|
});
|
|
1667
1375
|
}
|
|
1668
1376
|
}
|
|
1669
|
-
//#endregion
|
|
1670
|
-
//#region src/sources/llms.ts
|
|
1671
|
-
/**
|
|
1672
|
-
* Check for llms.txt at a docs URL, returns the llms.txt URL if found
|
|
1673
|
-
*/
|
|
1674
1377
|
async function fetchLlmsUrl(docsUrl) {
|
|
1675
1378
|
const llmsUrl = `${new URL(docsUrl).origin}/llms.txt`;
|
|
1676
1379
|
if (await verifyUrl(llmsUrl)) return llmsUrl;
|
|
1677
1380
|
return null;
|
|
1678
1381
|
}
|
|
1679
|
-
/**
|
|
1680
|
-
* Fetch and parse llms.txt content
|
|
1681
|
-
*/
|
|
1682
1382
|
async function fetchLlmsTxt(url) {
|
|
1683
1383
|
const content = await fetchText(url);
|
|
1684
1384
|
if (!content || content.length < 50) return null;
|
|
@@ -1687,16 +1387,9 @@ async function fetchLlmsTxt(url) {
|
|
|
1687
1387
|
links: parseMarkdownLinks(content)
|
|
1688
1388
|
};
|
|
1689
1389
|
}
|
|
1690
|
-
/**
|
|
1691
|
-
* Parse markdown links from llms.txt to get .md file paths
|
|
1692
|
-
*/
|
|
1693
1390
|
function parseMarkdownLinks(content) {
|
|
1694
1391
|
return extractLinks(content).filter((l) => l.url.endsWith(".md"));
|
|
1695
1392
|
}
|
|
1696
|
-
/**
|
|
1697
|
-
* Download all .md files referenced in llms.txt
|
|
1698
|
-
*/
|
|
1699
|
-
/** Reject non-https URLs and private/link-local IPs */
|
|
1700
1393
|
function isSafeUrl(url) {
|
|
1701
1394
|
try {
|
|
1702
1395
|
const parsed = new URL(url);
|
|
@@ -1726,10 +1419,6 @@ async function downloadLlmsDocs(llmsContent, baseUrl, onProgress) {
|
|
|
1726
1419
|
return null;
|
|
1727
1420
|
})))).filter((d) => d !== null);
|
|
1728
1421
|
}
|
|
1729
|
-
/**
|
|
1730
|
-
* Normalize llms.txt links to relative paths for local access
|
|
1731
|
-
* Handles: absolute URLs, root-relative paths, and relative paths
|
|
1732
|
-
*/
|
|
1733
1422
|
function normalizeLlmsLinks(content, baseUrl) {
|
|
1734
1423
|
let normalized = content;
|
|
1735
1424
|
if (baseUrl) {
|
|
@@ -1739,10 +1428,6 @@ function normalizeLlmsLinks(content, baseUrl) {
|
|
|
1739
1428
|
normalized = normalized.replace(/\]\(\/([^)]+\.md)\)/g, "](./docs/$1)");
|
|
1740
1429
|
return normalized;
|
|
1741
1430
|
}
|
|
1742
|
-
/**
|
|
1743
|
-
* Extract sections from llms-full.txt by URL patterns
|
|
1744
|
-
* Format: ---\nurl: /path.md\n---\n<content>\n\n---\nurl: ...
|
|
1745
|
-
*/
|
|
1746
1431
|
function extractSections(content, patterns) {
|
|
1747
1432
|
const sections = [];
|
|
1748
1433
|
const parts = content.split(/\n---\n/);
|
|
@@ -1758,16 +1443,8 @@ function extractSections(content, patterns) {
|
|
|
1758
1443
|
if (sections.length === 0) return null;
|
|
1759
1444
|
return sections.join("\n\n---\n\n");
|
|
1760
1445
|
}
|
|
1761
|
-
//#endregion
|
|
1762
|
-
//#region src/sources/github.ts
|
|
1763
|
-
/** Minimum git-doc file count to prefer over llms.txt */
|
|
1764
1446
|
const MIN_GIT_DOCS = 5;
|
|
1765
|
-
/** True when git-docs exist but are too few to be useful (< MIN_GIT_DOCS) */
|
|
1766
1447
|
const isShallowGitDocs = (n) => n > 0 && n < 5;
|
|
1767
|
-
/**
|
|
1768
|
-
* List files at a git ref. Tries ungh.cc first (fast, no rate limits),
|
|
1769
|
-
* falls back to GitHub API for private repos.
|
|
1770
|
-
*/
|
|
1771
1448
|
async function listFilesAtRef(owner, repo, ref) {
|
|
1772
1449
|
if (!isKnownPrivateRepo(owner, repo)) {
|
|
1773
1450
|
const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/files/${ref}`).catch(() => null);
|
|
@@ -1780,10 +1457,6 @@ async function listFilesAtRef(owner, repo, ref) {
|
|
|
1780
1457
|
}
|
|
1781
1458
|
return [];
|
|
1782
1459
|
}
|
|
1783
|
-
/**
|
|
1784
|
-
* Find git tag for a version by checking if ungh can list files at that ref.
|
|
1785
|
-
* Tries v{version}, {version}, and optionally {packageName}@{version} (changeset convention).
|
|
1786
|
-
*/
|
|
1787
1460
|
async function findGitTag(owner, repo, version, packageName, branchHint) {
|
|
1788
1461
|
const candidates = [`v${version}`, version];
|
|
1789
1462
|
if (packageName) candidates.push(`${packageName}@${version}`);
|
|
@@ -1815,9 +1488,6 @@ async function findGitTag(owner, repo, version, packageName, branchHint) {
|
|
|
1815
1488
|
}
|
|
1816
1489
|
return null;
|
|
1817
1490
|
}
|
|
1818
|
-
/**
|
|
1819
|
-
* Fetch releases from ungh.cc first, fall back to GitHub API for private repos.
|
|
1820
|
-
*/
|
|
1821
1491
|
async function fetchUnghReleases(owner, repo) {
|
|
1822
1492
|
if (!isKnownPrivateRepo(owner, repo)) {
|
|
1823
1493
|
const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`).catch(() => null);
|
|
@@ -1833,16 +1503,10 @@ async function fetchUnghReleases(owner, repo) {
|
|
|
1833
1503
|
}
|
|
1834
1504
|
return [];
|
|
1835
1505
|
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Find the latest release tag matching `{packageName}@*`.
|
|
1838
|
-
*/
|
|
1839
1506
|
async function findLatestReleaseTag(owner, repo, packageName) {
|
|
1840
1507
|
const prefix = `${packageName}@`;
|
|
1841
1508
|
return (await fetchUnghReleases(owner, repo)).find((r) => r.tag.startsWith(prefix))?.tag ?? null;
|
|
1842
1509
|
}
|
|
1843
|
-
/**
|
|
1844
|
-
* Filter file paths by prefix and md/mdx extension
|
|
1845
|
-
*/
|
|
1846
1510
|
function filterDocFiles(files, pathPrefix) {
|
|
1847
1511
|
return files.filter((f) => f.startsWith(pathPrefix) && /\.(?:md|mdx)$/.test(f));
|
|
1848
1512
|
}
|
|
@@ -1856,12 +1520,6 @@ const FRAMEWORK_NAMES = new Set([
|
|
|
1856
1520
|
"lit",
|
|
1857
1521
|
"qwik"
|
|
1858
1522
|
]);
|
|
1859
|
-
/**
|
|
1860
|
-
* Filter out docs for other frameworks when the package targets a specific one.
|
|
1861
|
-
* e.g. @tanstack/vue-query → keep vue + shared docs, exclude react/solid/angular
|
|
1862
|
-
* Uses word-boundary matching to catch all path conventions:
|
|
1863
|
-
* framework/react/, 0.react/, api/ai-react.md, react-native.mdx, etc.
|
|
1864
|
-
*/
|
|
1865
1523
|
function filterFrameworkDocs(files, packageName) {
|
|
1866
1524
|
if (!packageName) return files;
|
|
1867
1525
|
const shortName = packageName.replace(/^@.*\//, "");
|
|
@@ -1871,14 +1529,12 @@ function filterFrameworkDocs(files, packageName) {
|
|
|
1871
1529
|
const excludePattern = new RegExp(`\\b(?:${otherFrameworks.join("|")})\\b`);
|
|
1872
1530
|
return files.filter((f) => !excludePattern.test(f));
|
|
1873
1531
|
}
|
|
1874
|
-
/** Known noise paths to exclude from doc discovery */
|
|
1875
1532
|
const NOISE_PATTERNS = [
|
|
1876
1533
|
/^\.changeset\//,
|
|
1877
1534
|
/CHANGELOG\.md$/i,
|
|
1878
1535
|
/CONTRIBUTING\.md$/i,
|
|
1879
1536
|
/^\.github\//
|
|
1880
1537
|
];
|
|
1881
|
-
/** Directories to exclude from "best directory" heuristic */
|
|
1882
1538
|
const EXCLUDE_DIRS = new Set([
|
|
1883
1539
|
"test",
|
|
1884
1540
|
"tests",
|
|
@@ -1897,7 +1553,6 @@ const EXCLUDE_DIRS = new Set([
|
|
|
1897
1553
|
"mocks",
|
|
1898
1554
|
"__mocks__"
|
|
1899
1555
|
]);
|
|
1900
|
-
/** Directory names that suggest documentation */
|
|
1901
1556
|
const DOC_DIR_BONUS = new Set([
|
|
1902
1557
|
"docs",
|
|
1903
1558
|
"documentation",
|
|
@@ -1910,38 +1565,19 @@ const DOC_DIR_BONUS = new Set([
|
|
|
1910
1565
|
"manual",
|
|
1911
1566
|
"api"
|
|
1912
1567
|
]);
|
|
1913
|
-
/**
|
|
1914
|
-
* Check if a path contains any excluded directory
|
|
1915
|
-
*/
|
|
1916
1568
|
function hasExcludedDir(path) {
|
|
1917
1569
|
return path.split("/").some((p) => EXCLUDE_DIRS.has(p.toLowerCase()));
|
|
1918
1570
|
}
|
|
1919
|
-
/**
|
|
1920
|
-
* Get the depth of a path (number of directory levels)
|
|
1921
|
-
*/
|
|
1922
1571
|
function getPathDepth(path) {
|
|
1923
1572
|
return path.split("/").filter(Boolean).length;
|
|
1924
1573
|
}
|
|
1925
|
-
/**
|
|
1926
|
-
* Check if path contains a doc-related directory name
|
|
1927
|
-
*/
|
|
1928
1574
|
function hasDocDirBonus(path) {
|
|
1929
1575
|
return path.split("/").some((p) => DOC_DIR_BONUS.has(p.toLowerCase()));
|
|
1930
1576
|
}
|
|
1931
|
-
/**
|
|
1932
|
-
* Score a directory for doc likelihood.
|
|
1933
|
-
* Higher = better. Formula: count * nameBonus / depth
|
|
1934
|
-
*/
|
|
1935
1577
|
function scoreDocDir(dir, fileCount) {
|
|
1936
1578
|
const depth = getPathDepth(dir) || 1;
|
|
1937
1579
|
return fileCount * (hasDocDirBonus(dir) ? 1.5 : 1) / depth;
|
|
1938
1580
|
}
|
|
1939
|
-
/**
|
|
1940
|
-
* Discover doc files in non-standard locations.
|
|
1941
|
-
* First tries to scope to sub-package dir in monorepos.
|
|
1942
|
-
* Then looks for clusters of md/mdx files in paths containing /docs/.
|
|
1943
|
-
* Falls back to finding the directory with the most markdown files (≥5).
|
|
1944
|
-
*/
|
|
1945
1581
|
function discoverDocFiles(allFiles, packageName) {
|
|
1946
1582
|
const mdFiles = allFiles.filter((f) => /\.(?:md|mdx)$/.test(f)).filter((f) => !NOISE_PATTERNS.some((p) => p.test(f))).filter((f) => f.includes("/"));
|
|
1947
1583
|
if (packageName?.includes("/")) {
|
|
@@ -1990,16 +1626,9 @@ function discoverDocFiles(allFiles, packageName) {
|
|
|
1990
1626
|
prefix: best.dir
|
|
1991
1627
|
};
|
|
1992
1628
|
}
|
|
1993
|
-
/**
|
|
1994
|
-
* List markdown files in a folder at a specific git ref
|
|
1995
|
-
*/
|
|
1996
1629
|
async function listDocsAtRef(owner, repo, ref, pathPrefix = "docs/") {
|
|
1997
1630
|
return filterDocFiles(await listFilesAtRef(owner, repo, ref), pathPrefix);
|
|
1998
1631
|
}
|
|
1999
|
-
/**
|
|
2000
|
-
* Fetch versioned docs from GitHub repo's docs/ folder.
|
|
2001
|
-
* Pass packageName to check doc overrides (e.g. vue -> vuejs/docs).
|
|
2002
|
-
*/
|
|
2003
1632
|
async function fetchGitDocs(owner, repo, version, packageName, repoUrl) {
|
|
2004
1633
|
const override = packageName ? getDocOverride(packageName) : void 0;
|
|
2005
1634
|
if (override) {
|
|
@@ -2039,18 +1668,9 @@ async function fetchGitDocs(owner, repo, version, packageName, repoUrl) {
|
|
|
2039
1668
|
fallback: tag.fallback
|
|
2040
1669
|
};
|
|
2041
1670
|
}
|
|
2042
|
-
/**
|
|
2043
|
-
* Strip file extension (.md, .mdx) and leading slash from a path
|
|
2044
|
-
*/
|
|
2045
1671
|
function normalizePath(p) {
|
|
2046
1672
|
return p.replace(/^\//, "").replace(/\.(?:md|mdx)$/, "");
|
|
2047
1673
|
}
|
|
2048
|
-
/**
|
|
2049
|
-
* Validate that discovered git docs are relevant by cross-referencing llms.txt links
|
|
2050
|
-
* against the repo file tree. Uses extensionless suffix matching to handle monorepo nesting.
|
|
2051
|
-
*
|
|
2052
|
-
* Returns { isValid, matchRatio } where isValid = matchRatio >= 0.3
|
|
2053
|
-
*/
|
|
2054
1674
|
function validateGitDocsWithLlms(llmsLinks, repoFiles) {
|
|
2055
1675
|
if (llmsLinks.length === 0) return {
|
|
2056
1676
|
isValid: true,
|
|
@@ -2076,10 +1696,6 @@ function validateGitDocsWithLlms(llmsLinks, repoFiles) {
|
|
|
2076
1696
|
matchRatio
|
|
2077
1697
|
};
|
|
2078
1698
|
}
|
|
2079
|
-
/**
|
|
2080
|
-
* Verify a GitHub repo is the source for an npm package by checking package.json name field.
|
|
2081
|
-
* Checks root first, then common monorepo paths (packages/{shortName}, packages/{name}).
|
|
2082
|
-
*/
|
|
2083
1699
|
async function verifyNpmRepo(owner, repo, packageName) {
|
|
2084
1700
|
const base = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD`;
|
|
2085
1701
|
const paths = [
|
|
@@ -2138,19 +1754,12 @@ async function searchGitHubRepo(packageName) {
|
|
|
2138
1754
|
}
|
|
2139
1755
|
return null;
|
|
2140
1756
|
}
|
|
2141
|
-
/**
|
|
2142
|
-
* Fetch GitHub repo metadata to get website URL.
|
|
2143
|
-
* Pass packageName to check doc overrides first (avoids API call).
|
|
2144
|
-
*/
|
|
2145
1757
|
async function fetchGitHubRepoMeta(owner, repo, packageName) {
|
|
2146
1758
|
const override = packageName ? getDocOverride(packageName) : void 0;
|
|
2147
1759
|
if (override?.homepage) return { homepage: override.homepage };
|
|
2148
1760
|
const data = await ghApi(`repos/${owner}/${repo}`) ?? await $fetch(`https://api.github.com/repos/${owner}/${repo}`).catch(() => null);
|
|
2149
1761
|
return data?.homepage ? { homepage: data.homepage } : null;
|
|
2150
1762
|
}
|
|
2151
|
-
/**
|
|
2152
|
-
* Resolve README URL for a GitHub repo, returns ungh:// pseudo-URL or raw URL
|
|
2153
|
-
*/
|
|
2154
1763
|
async function fetchReadme(owner, repo, subdir, ref) {
|
|
2155
1764
|
const branch = ref || "main";
|
|
2156
1765
|
if (!isKnownPrivateRepo(owner, repo)) {
|
|
@@ -2177,9 +1786,6 @@ async function fetchReadme(owner, repo, subdir, ref) {
|
|
|
2177
1786
|
}
|
|
2178
1787
|
return null;
|
|
2179
1788
|
}
|
|
2180
|
-
/**
|
|
2181
|
-
* Fetch README content from ungh:// pseudo-URL, file:// URL, or regular URL
|
|
2182
|
-
*/
|
|
2183
1789
|
async function fetchReadmeContent(url) {
|
|
2184
1790
|
if (url.startsWith("file://")) {
|
|
2185
1791
|
const filePath = fileURLToPath(url);
|
|
@@ -2210,10 +1816,6 @@ async function fetchReadmeContent(url) {
|
|
|
2210
1816
|
if (url.includes("raw.githubusercontent.com")) return fetchGitHubRaw(url);
|
|
2211
1817
|
return fetchText(url);
|
|
2212
1818
|
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Resolve a GitHub repo into a ResolvedPackage (no npm registry needed).
|
|
2215
|
-
* Fetches repo meta, latest release version, git docs, README, and llms.txt.
|
|
2216
|
-
*/
|
|
2217
1819
|
async function resolveGitHubRepo(owner, repo, onProgress) {
|
|
2218
1820
|
onProgress?.("Fetching repo metadata");
|
|
2219
1821
|
const repoUrl = `https://github.com/${owner}/${repo}`;
|
|
@@ -2255,12 +1857,6 @@ async function resolveGitHubRepo(owner, repo, onProgress) {
|
|
|
2255
1857
|
llmsUrl
|
|
2256
1858
|
};
|
|
2257
1859
|
}
|
|
2258
|
-
//#endregion
|
|
2259
|
-
//#region src/sources/npm.ts
|
|
2260
|
-
/**
|
|
2261
|
-
* Search npm registry for packages matching a query.
|
|
2262
|
-
* Used as a fallback when direct package lookup fails.
|
|
2263
|
-
*/
|
|
2264
1860
|
async function searchNpmPackages(query, size = 5) {
|
|
2265
1861
|
const data = await $fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`).catch(() => null);
|
|
2266
1862
|
if (!data?.objects?.length) return [];
|
|
@@ -2270,17 +1866,11 @@ async function searchNpmPackages(query, size = 5) {
|
|
|
2270
1866
|
version: o.package.version
|
|
2271
1867
|
}));
|
|
2272
1868
|
}
|
|
2273
|
-
/**
|
|
2274
|
-
* Fetch package info from npm registry
|
|
2275
|
-
*/
|
|
2276
1869
|
async function fetchNpmPackage(packageName) {
|
|
2277
1870
|
const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
|
|
2278
1871
|
if (data) return data;
|
|
2279
1872
|
return $fetch(`https://registry.npmjs.org/${packageName}/latest`).catch(() => null);
|
|
2280
1873
|
}
|
|
2281
|
-
/**
|
|
2282
|
-
* Fetch release date and dist-tags from npm registry
|
|
2283
|
-
*/
|
|
2284
1874
|
async function fetchNpmRegistryMeta(packageName, version) {
|
|
2285
1875
|
const { name: barePackageName } = parsePackageSpec(packageName);
|
|
2286
1876
|
const data = await $fetch(`https://registry.npmjs.org/${barePackageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null);
|
|
@@ -2294,10 +1884,6 @@ async function fetchNpmRegistryMeta(packageName, version) {
|
|
|
2294
1884
|
distTags
|
|
2295
1885
|
};
|
|
2296
1886
|
}
|
|
2297
|
-
/**
|
|
2298
|
-
* Shared GitHub resolution cascade: git docs → repo meta (homepage) → README.
|
|
2299
|
-
* Used for both "repo URL found in package.json" and "repo URL found via search" paths.
|
|
2300
|
-
*/
|
|
2301
1887
|
async function resolveGitHub(gh, targetVersion, pkg, result, attempts, onProgress, opts) {
|
|
2302
1888
|
let allFiles;
|
|
2303
1889
|
if (targetVersion) {
|
|
@@ -2356,15 +1942,9 @@ async function resolveGitHub(gh, targetVersion, pkg, result, attempts, onProgres
|
|
|
2356
1942
|
});
|
|
2357
1943
|
return allFiles;
|
|
2358
1944
|
}
|
|
2359
|
-
/**
|
|
2360
|
-
* Resolve documentation URL for a package (legacy - returns null on failure)
|
|
2361
|
-
*/
|
|
2362
1945
|
async function resolvePackageDocs(packageName, options = {}) {
|
|
2363
1946
|
return (await resolvePackageDocsWithAttempts(packageName, options)).package;
|
|
2364
1947
|
}
|
|
2365
|
-
/**
|
|
2366
|
-
* Resolve documentation URL for a package with attempt tracking
|
|
2367
|
-
*/
|
|
2368
1948
|
async function resolvePackageDocsWithAttempts(packageName, options = {}) {
|
|
2369
1949
|
const attempts = [];
|
|
2370
1950
|
const { onProgress } = options;
|
|
@@ -2500,9 +2080,6 @@ async function resolvePackageDocsWithAttempts(packageName, options = {}) {
|
|
|
2500
2080
|
registryVersion: pkg.version
|
|
2501
2081
|
};
|
|
2502
2082
|
}
|
|
2503
|
-
/**
|
|
2504
|
-
* Parse version specifier, handling protocols like link:, workspace:, npm:, file:
|
|
2505
|
-
*/
|
|
2506
2083
|
function parseVersionSpecifier(name, version, cwd) {
|
|
2507
2084
|
if (version.startsWith("link:")) {
|
|
2508
2085
|
const linkedPkg = readPackageJsonSafe(join(resolve(cwd, version.slice(5)), "package.json"));
|
|
@@ -2537,10 +2114,6 @@ function parseVersionSpecifier(name, version, cwd) {
|
|
|
2537
2114
|
};
|
|
2538
2115
|
return null;
|
|
2539
2116
|
}
|
|
2540
|
-
/**
|
|
2541
|
-
* Resolve the actual installed version of a package by finding its package.json
|
|
2542
|
-
* via mlly's resolvePathSync. Works regardless of package manager or version protocol.
|
|
2543
|
-
*/
|
|
2544
2117
|
function resolveInstalledVersion(name, cwd) {
|
|
2545
2118
|
try {
|
|
2546
2119
|
return readPackageJsonSafe(resolvePathSync(`${name}/package.json`, { url: cwd }))?.parsed.version || null;
|
|
@@ -2558,9 +2131,6 @@ function resolveInstalledVersion(name, cwd) {
|
|
|
2558
2131
|
return null;
|
|
2559
2132
|
}
|
|
2560
2133
|
}
|
|
2561
|
-
/**
|
|
2562
|
-
* Read package.json dependencies with versions
|
|
2563
|
-
*/
|
|
2564
2134
|
async function readLocalDependencies(cwd) {
|
|
2565
2135
|
const result = readPackageJsonSafe(join(cwd, "package.json"));
|
|
2566
2136
|
if (!result) throw new Error("No package.json found in current directory");
|
|
@@ -2576,9 +2146,6 @@ async function readLocalDependencies(cwd) {
|
|
|
2576
2146
|
}
|
|
2577
2147
|
return results;
|
|
2578
2148
|
}
|
|
2579
|
-
/**
|
|
2580
|
-
* Read package info from a local path (for link: deps)
|
|
2581
|
-
*/
|
|
2582
2149
|
function readLocalPackageInfo(localPath) {
|
|
2583
2150
|
const result = readPackageJsonSafe(join(localPath, "package.json"));
|
|
2584
2151
|
if (!result) return null;
|
|
@@ -2594,9 +2161,6 @@ function readLocalPackageInfo(localPath) {
|
|
|
2594
2161
|
localPath
|
|
2595
2162
|
};
|
|
2596
2163
|
}
|
|
2597
|
-
/**
|
|
2598
|
-
* Resolve docs for a local package (link: dependency)
|
|
2599
|
-
*/
|
|
2600
2164
|
async function resolveLocalPackageDocs(localPath) {
|
|
2601
2165
|
const info = readLocalPackageInfo(localPath);
|
|
2602
2166
|
if (!info) return null;
|
|
@@ -2626,13 +2190,6 @@ async function resolveLocalPackageDocs(localPath) {
|
|
|
2626
2190
|
if (!result.readmeUrl && !result.gitDocsUrl) return null;
|
|
2627
2191
|
return result;
|
|
2628
2192
|
}
|
|
2629
|
-
/**
|
|
2630
|
-
* Download and extract npm package tarball to cache directory.
|
|
2631
|
-
* Used when the package isn't available in node_modules.
|
|
2632
|
-
*
|
|
2633
|
-
* Extracts to: ~/.skilld/references/<pkg>@<version>/pkg/
|
|
2634
|
-
* Returns the extracted directory path, or null on failure.
|
|
2635
|
-
*/
|
|
2636
2193
|
async function fetchPkgDist(name, version) {
|
|
2637
2194
|
const cacheDir = getCacheDir(name, version);
|
|
2638
2195
|
const pkgDir = join(cacheDir, "pkg");
|
|
@@ -2700,23 +2257,16 @@ async function fetchPkgDist(name, version) {
|
|
|
2700
2257
|
} catch {}
|
|
2701
2258
|
}
|
|
2702
2259
|
}
|
|
2703
|
-
/**
|
|
2704
|
-
* Fetch just the latest version string from npm (lightweight)
|
|
2705
|
-
*/
|
|
2706
2260
|
async function fetchLatestVersion(packageName) {
|
|
2707
2261
|
const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
|
|
2708
2262
|
if (data?.version) return data.version;
|
|
2709
2263
|
return (await $fetch(`https://registry.npmjs.org/${packageName}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } }).catch(() => null))?.["dist-tags"]?.latest || null;
|
|
2710
2264
|
}
|
|
2711
|
-
/**
|
|
2712
|
-
* Get installed skill version from SKILL.md
|
|
2713
|
-
*/
|
|
2714
2265
|
function getInstalledSkillVersion(skillDir) {
|
|
2715
2266
|
const skillPath = join(skillDir, "SKILL.md");
|
|
2716
2267
|
if (!existsSync(skillPath)) return null;
|
|
2717
2268
|
return readFileSync(skillPath, "utf-8").match(/^version:\s*"?([^"\n]+)"?/m)?.[1] || null;
|
|
2718
2269
|
}
|
|
2719
|
-
//#endregion
|
|
2720
2270
|
export { isGitHubRepoUrl as $, parseGitSkillInput as A, isGhAvailable as B, downloadLlmsDocs as C, normalizeLlmsLinks as D, fetchLlmsUrl as E, formatDiscussionAsMarkdown as F, fetchReleaseNotes as G, toCrawlPattern as H, generateDiscussionIndex as I, parseSemver as J, generateReleaseIndex as K, fetchGitHubIssues as L, resolveEntryFiles as M, generateDocsIndex as N, parseMarkdownLinks as O, fetchGitHubDiscussions as P, fetchText as Q, formatIssueAsMarkdown as R, validateGitDocsWithLlms as S, fetchLlmsTxt as T, fetchBlogReleases as U, fetchCrawledDocs as V, compareSemver as W, extractBranchHint as X, $fetch as Y, fetchGitHubRaw as Z, fetchReadme as _, getInstalledSkillVersion as a, isShallowGitDocs as b, readLocalPackageInfo as c, resolvePackageDocs as d, normalizeRepoUrl as et, resolvePackageDocsWithAttempts as f, fetchGitHubRepoMeta as g, fetchGitDocs as h, fetchPkgDist as i, parseSkillFrontmatterName as j, fetchGitSkills as k, resolveInstalledVersion as l, MIN_GIT_DOCS as m, fetchNpmPackage as n, parsePackageSpec as nt, parseVersionSpecifier as o, searchNpmPackages as p, isPrerelease as q, fetchNpmRegistryMeta as r, verifyUrl as rt, readLocalDependencies as s, fetchLatestVersion as t, parseGitHubUrl as tt, resolveLocalPackageDocs as u, fetchReadmeContent as v, extractSections as w, resolveGitHubRepo as x, filterFrameworkDocs as y, generateIssueIndex as z };
|
|
2721
2271
|
|
|
2722
2272
|
//# sourceMappingURL=sources.mjs.map
|