nuxt-skill-hub 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useLogger, defineNuxtModule } from '@nuxt/kit';
2
- import { isCI, isTest } from 'std-env';
2
+ import { isAgent, agent, isCI, isTest } from 'std-env';
3
3
  import { promises, existsSync, lstatSync, writeFileSync, readFileSync } from 'node:fs';
4
4
  import { join, basename, resolve, dirname, relative, isAbsolute } from 'pathe';
5
5
  import { fileURLToPath } from 'mlly';
@@ -8,15 +8,16 @@ import matter from 'gray-matter';
8
8
  import { readPackageJSON, findWorkspaceDir, resolvePackageJSON, resolveLockfile } from 'pkg-types';
9
9
  import { transform } from 'automd';
10
10
  import { downloadTemplate } from 'giget';
11
- import { detectInstalledAgents, expandPath, detectCurrentAgent, getAgentIds, getAgentConfig } from 'unagent/env';
11
+ import { getAgentConfig, detectInstalledAgents, expandPath, detectCurrentAgent, getAgentIds } from 'unagent/env';
12
12
  import { ofetch } from 'ofetch';
13
13
  import { createHash } from 'node:crypto';
14
+ import { isIP } from 'node:net';
14
15
  import { hash } from 'ohash';
15
16
  import { glob } from 'tinyglobby';
16
17
  import { createConsola } from 'consola';
17
18
  import { colorize } from 'consola/utils';
18
19
 
19
- const version = "0.0.6";
20
+ const version = "0.0.8";
20
21
  const packageJson = {
21
22
  version: version};
22
23
 
@@ -146,6 +147,8 @@ function getSourceLabel(sourceKind) {
146
147
  return "Installed package skill";
147
148
  case "github":
148
149
  return "Resolved module skill";
150
+ case "wellKnown":
151
+ return "Docs-discovered skill";
149
152
  case "generated":
150
153
  return "Metadata-routed skill";
151
154
  }
@@ -206,8 +209,9 @@ function relativeModuleLink(entry, prefix) {
206
209
  function groupModuleEntries(entries) {
207
210
  const officialUpstream = entries.filter((entry) => entry.sourceKind === "dist");
208
211
  const githubResolved = entries.filter((entry) => entry.sourceKind === "github");
212
+ const wellKnownResolved = entries.filter((entry) => entry.sourceKind === "wellKnown");
209
213
  const metadataRouted = entries.filter((entry) => entry.sourceKind === "generated");
210
- return { officialUpstream, githubResolved, metadataRouted };
214
+ return { officialUpstream, githubResolved, wellKnownResolved, metadataRouted };
211
215
  }
212
216
  function hasModuleGuidance(entries, skipped = []) {
213
217
  return entries.length > 0 || skipped.length > 0;
@@ -330,6 +334,7 @@ function createSkillMapSections(metadata, entries, skipped = [], includeModuleAu
330
334
  const moduleSections = [
331
335
  renderModuleGroup("Official upstream skills", grouped.officialUpstream, "./references/modules/"),
332
336
  renderModuleGroup("Resolved module skills", grouped.githubResolved, "./references/modules/"),
337
+ renderModuleGroup("Docs-discovered skills", grouped.wellKnownResolved, "./references/modules/"),
333
338
  renderModuleGroup("Metadata-routed skills", grouped.metadataRouted, "./references/modules/"),
334
339
  renderSkippedEntries(skipped)
335
340
  ].filter(Boolean);
@@ -494,24 +499,15 @@ function resolveSkillsDir(target, config) {
494
499
  }
495
500
  return void 0;
496
501
  }
497
- function normalizeTarget(target) {
498
- return target.trim();
499
- }
500
- function getRawTargetConfig(target) {
501
- return getAgentConfig(target);
502
- }
503
502
  function getSupportedTargets() {
504
- return getAgentIds().filter((target) => {
505
- const config = getRawTargetConfig(target);
506
- return Boolean(resolveSkillsDir(target, config));
507
- }).sort((a, b) => a.localeCompare(b));
503
+ return getAgentIds().filter((target) => Boolean(resolveSkillsDir(target, getAgentConfig(target)))).sort((a, b) => a.localeCompare(b));
508
504
  }
509
505
  function resolveAgentTargetConfig(target) {
510
- const normalized = normalizeTarget(target);
506
+ const normalized = target.trim();
511
507
  if (!normalized) {
512
508
  return void 0;
513
509
  }
514
- const config = getRawTargetConfig(normalized);
510
+ const config = getAgentConfig(normalized);
515
511
  const skillsDir = resolveSkillsDir(normalized, config);
516
512
  if (!config || !skillsDir) {
517
513
  return void 0;
@@ -523,11 +519,11 @@ function resolveAgentTargetConfig(target) {
523
519
  };
524
520
  }
525
521
  function validateTargets(targets) {
526
- const uniqueTargets = Array.from(new Set(targets.map(normalizeTarget).filter(Boolean)));
522
+ const uniqueTargets = Array.from(new Set(targets.map((target) => target.trim()).filter(Boolean)));
527
523
  const valid = [];
528
524
  const invalid = [];
529
525
  for (const target of uniqueTargets) {
530
- const config = getRawTargetConfig(target);
526
+ const config = getAgentConfig(target);
531
527
  if (!config) {
532
528
  invalid.push({
533
529
  target,
@@ -562,6 +558,24 @@ const MANAGED_HINT_START = "<!-- nuxt-skill-hub:start -->";
562
558
  const MANAGED_HINT_END = "<!-- nuxt-skill-hub:end -->";
563
559
  const SKILL_NAME_MAX_LENGTH = 64;
564
560
  const SKILL_NAME_PATTERN = /^[a-z0-9-]+$/;
561
+ async function mapWithConcurrency(items, concurrency, mapper) {
562
+ if (!items.length) {
563
+ return [];
564
+ }
565
+ const results = [];
566
+ let nextIndex = 0;
567
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
568
+ while (true) {
569
+ const index = nextIndex++;
570
+ if (index >= items.length) {
571
+ return;
572
+ }
573
+ results[index] = await mapper(items[index], index);
574
+ }
575
+ });
576
+ await Promise.all(workers);
577
+ return results;
578
+ }
565
579
  async function pathExists(path) {
566
580
  try {
567
581
  await promises.access(path);
@@ -746,6 +760,22 @@ async function discoverInstalledPackageFromDirectory(directory) {
746
760
  }
747
761
  return readInstalledPackageMetadata(packageJsonPath);
748
762
  }
763
+ async function collectInstalledModulePackages(modules, rootDir) {
764
+ const specifiers = Array.from(new Set(
765
+ (modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry))
766
+ ));
767
+ const resolved = await Promise.all(
768
+ specifiers.map((specifier) => discoverInstalledPackageFromSpecifier(specifier, rootDir))
769
+ );
770
+ const seenPackageRoots = /* @__PURE__ */ new Set();
771
+ const packages = [];
772
+ for (const pkg of resolved) {
773
+ if (!pkg || seenPackageRoots.has(pkg.packageRoot)) continue;
774
+ seenPackageRoots.add(pkg.packageRoot);
775
+ packages.push(pkg);
776
+ }
777
+ return packages;
778
+ }
749
779
  async function readInstalledPackageMetadata(packageJsonPath, fallbackSpecifier) {
750
780
  const pkg = await readPackageJSON(packageJsonPath);
751
781
  return {
@@ -863,15 +893,15 @@ async function validateContribution(contribution) {
863
893
  };
864
894
  }
865
895
  async function validateResolvedContributions(contributions) {
896
+ const validations = await Promise.all(contributions.map(validateContribution));
866
897
  const valid = [];
867
898
  const issues = [];
868
- for (const contribution of contributions) {
869
- const validation = await validateContribution(contribution);
870
- if (!validation.issues.length) {
871
- valid.push(validation.contribution);
899
+ for (const validation of validations) {
900
+ if (validation.issues.length) {
901
+ issues.push(...validation.issues);
872
902
  continue;
873
903
  }
874
- issues.push(...validation.issues);
904
+ valid.push(validation.contribution);
875
905
  }
876
906
  return {
877
907
  contributions: sortAndDedupeContributions(valid),
@@ -1040,7 +1070,7 @@ function formatConflictWarning(conflict) {
1040
1070
  return `Found ${conflict.skillNames.length} standalone skill(s) that may conflict with nuxt-skill-hub: ${conflict.skillNames.join(", ")} (global: ${conflict.globalDir}). Remove with: npx skills remove --global`;
1041
1071
  }
1042
1072
 
1043
- const GITHUB_OVERRIDES = [
1073
+ const PACKAGE_OVERRIDES = [
1044
1074
  {
1045
1075
  packageName: "@nuxt/ui",
1046
1076
  repo: "nuxt/ui",
@@ -1054,14 +1084,24 @@ const GITHUB_OVERRIDES = [
1054
1084
  ref: "main",
1055
1085
  path: "skills/vueuse-functions",
1056
1086
  skillName: "vueuse-functions"
1087
+ },
1088
+ {
1089
+ packageName: "docus",
1090
+ repo: "nuxt-content/docus",
1091
+ docsUrls: ["https://docus.dev/"]
1057
1092
  }
1058
1093
  ];
1059
- function findGitHubOverride(packageName) {
1060
- return GITHUB_OVERRIDES.find((entry) => entry.packageName === packageName);
1094
+ function findPackageOverride(packageName) {
1095
+ return PACKAGE_OVERRIDES.find((entry) => entry.packageName === packageName);
1061
1096
  }
1062
1097
 
1063
1098
  const GITHUB_API_BASE = "https://api.github.com";
1064
1099
  const UNGH_API_BASE = "https://ungh.cc";
1100
+ const githubDirectoryCache = /* @__PURE__ */ new Map();
1101
+ const githubDefaultBranchCache = /* @__PURE__ */ new Map();
1102
+ const githubFileTextCache = /* @__PURE__ */ new Map();
1103
+ const urlTextCache = /* @__PURE__ */ new Map();
1104
+ const urlBytesCache = /* @__PURE__ */ new Map();
1065
1105
  function githubFetchOptions(timeoutMs) {
1066
1106
  return {
1067
1107
  timeout: timeoutMs,
@@ -1071,6 +1111,9 @@ function githubFetchOptions(timeoutMs) {
1071
1111
  }
1072
1112
  };
1073
1113
  }
1114
+ function fetchStatus(error) {
1115
+ return typeof error === "object" && error && "status" in error ? Number(error.status) : void 0;
1116
+ }
1074
1117
  function encodeGitHubPath(path) {
1075
1118
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
1076
1119
  }
@@ -1096,6 +1139,15 @@ function toRepoPath(value) {
1096
1139
  }
1097
1140
  return null;
1098
1141
  }
1142
+ function memoize(cache, key, factory) {
1143
+ const cached = cache.get(key);
1144
+ if (cached) {
1145
+ return cached;
1146
+ }
1147
+ const pending = factory();
1148
+ cache.set(key, pending);
1149
+ return pending;
1150
+ }
1099
1151
  function parseGitHubRepo(input) {
1100
1152
  if (!input) {
1101
1153
  return null;
@@ -1109,25 +1161,27 @@ async function listGitHubDirectory(repo, ref, dirPath, timeoutMs) {
1109
1161
  }
1110
1162
  const normalizedDirPath = dirPath.replace(/^\/+|\/+$/g, "");
1111
1163
  const prefix = normalizedDirPath ? `${normalizedDirPath}/` : "";
1112
- try {
1113
- const data = await ofetch(
1114
- `${GITHUB_API_BASE}/repos/${repoPath}/git/trees/${encodeURIComponent(ref)}?recursive=1`,
1115
- githubFetchOptions(timeoutMs)
1116
- );
1117
- const entries = /* @__PURE__ */ new Set();
1118
- for (const entry of data.tree || []) {
1119
- if (entry.type !== "tree" || !entry.path.startsWith(prefix)) {
1120
- continue;
1121
- }
1122
- const remainder = entry.path.slice(prefix.length);
1123
- if (remainder && !remainder.includes("/")) {
1124
- entries.add(remainder);
1164
+ return await memoize(githubDirectoryCache, `${repoPath}::${ref}::${normalizedDirPath}`, async () => {
1165
+ try {
1166
+ const data = await ofetch(
1167
+ `${GITHUB_API_BASE}/repos/${repoPath}/git/trees/${encodeURIComponent(ref)}?recursive=1`,
1168
+ githubFetchOptions(timeoutMs)
1169
+ );
1170
+ const entries = /* @__PURE__ */ new Set();
1171
+ for (const entry of data.tree || []) {
1172
+ if (entry.type !== "tree" || !entry.path.startsWith(prefix)) {
1173
+ continue;
1174
+ }
1175
+ const remainder = entry.path.slice(prefix.length);
1176
+ if (remainder && !remainder.includes("/")) {
1177
+ entries.add(remainder);
1178
+ }
1125
1179
  }
1180
+ return Array.from(entries).sort((a, b) => a.localeCompare(b));
1181
+ } catch {
1182
+ return [];
1126
1183
  }
1127
- return Array.from(entries).sort((a, b) => a.localeCompare(b));
1128
- } catch {
1129
- return [];
1130
- }
1184
+ });
1131
1185
  }
1132
1186
  async function fetchGitHubDefaultBranch(repo, timeoutMs) {
1133
1187
  const repoPath = toRepoPath(repo);
@@ -1138,39 +1192,405 @@ async function fetchGitHubDefaultBranch(repo, timeoutMs) {
1138
1192
  if (!owner || !name) {
1139
1193
  return null;
1140
1194
  }
1141
- try {
1142
- const data = await ofetch(
1143
- `${UNGH_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
1144
- githubFetchOptions(timeoutMs)
1145
- );
1146
- return data.repo?.defaultBranch || null;
1147
- } catch {
1148
- return null;
1149
- }
1195
+ return await memoize(githubDefaultBranchCache, repoPath, async () => {
1196
+ try {
1197
+ const data = await ofetch(
1198
+ `${UNGH_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
1199
+ githubFetchOptions(timeoutMs)
1200
+ );
1201
+ return data.repo?.defaultBranch || null;
1202
+ } catch {
1203
+ return null;
1204
+ }
1205
+ });
1150
1206
  }
1151
1207
  async function fetchGitHubFileText(repo, ref, filePath, timeoutMs) {
1152
1208
  const repoPath = toRepoPath(repo);
1153
1209
  if (!repoPath) {
1154
1210
  return { ok: false, error: "Invalid GitHub repository format" };
1155
1211
  }
1156
- try {
1157
- const data = await ofetch(
1158
- `https://raw.githubusercontent.com/${repoPath}/${encodeURIComponent(ref)}/${encodeGitHubPath(filePath)}`,
1159
- {
1212
+ return await memoize(githubFileTextCache, `${repoPath}::${ref}::${filePath}`, async () => {
1213
+ try {
1214
+ const data = await ofetch(
1215
+ `https://raw.githubusercontent.com/${repoPath}/${encodeURIComponent(ref)}/${encodeGitHubPath(filePath)}`,
1216
+ {
1217
+ ...githubFetchOptions(timeoutMs),
1218
+ responseType: "text"
1219
+ }
1220
+ );
1221
+ return { ok: true, data };
1222
+ } catch (error) {
1223
+ return {
1224
+ ok: false,
1225
+ status: fetchStatus(error),
1226
+ error: error.message
1227
+ };
1228
+ }
1229
+ });
1230
+ }
1231
+ async function fetchUrlText(url, timeoutMs) {
1232
+ return await memoize(urlTextCache, url, async () => {
1233
+ try {
1234
+ const data = await ofetch(url, {
1160
1235
  ...githubFetchOptions(timeoutMs),
1161
1236
  responseType: "text"
1162
- }
1163
- );
1164
- return { ok: true, data };
1237
+ });
1238
+ return { ok: true, data };
1239
+ } catch (error) {
1240
+ return {
1241
+ ok: false,
1242
+ status: fetchStatus(error),
1243
+ error: error.message
1244
+ };
1245
+ }
1246
+ });
1247
+ }
1248
+ async function fetchUrlJson(url, timeoutMs) {
1249
+ const text = await fetchUrlText(url, timeoutMs);
1250
+ if (!text.ok) {
1251
+ return {
1252
+ ok: false,
1253
+ status: text.status,
1254
+ error: text.error
1255
+ };
1256
+ }
1257
+ try {
1258
+ return {
1259
+ ok: true,
1260
+ data: JSON.parse(text.data || "")
1261
+ };
1165
1262
  } catch (error) {
1166
- const status = typeof error === "object" && error && "status" in error ? Number(error.status) : void 0;
1167
1263
  return {
1168
1264
  ok: false,
1169
- status,
1170
1265
  error: error.message
1171
1266
  };
1172
1267
  }
1173
1268
  }
1269
+ async function fetchUrlBytes(url, timeoutMs) {
1270
+ return await memoize(urlBytesCache, url, async () => {
1271
+ try {
1272
+ const data = await ofetch(url, {
1273
+ ...githubFetchOptions(timeoutMs),
1274
+ responseType: "arrayBuffer"
1275
+ });
1276
+ return {
1277
+ ok: true,
1278
+ data: new Uint8Array(data)
1279
+ };
1280
+ } catch (error) {
1281
+ return {
1282
+ ok: false,
1283
+ status: fetchStatus(error),
1284
+ error: error.message
1285
+ };
1286
+ }
1287
+ });
1288
+ }
1289
+
1290
+ const WELL_KNOWN_DISCOVERY_V2_SCHEMA = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
1291
+ const WELL_KNOWN_DISCOVERY_V2_INDEX_PATH = "/.well-known/agent-skills/index.json";
1292
+ function makeSkip$1(packageName, skillName, reason) {
1293
+ return {
1294
+ packageName,
1295
+ skillName,
1296
+ reason,
1297
+ sourceKind: "wellKnown"
1298
+ };
1299
+ }
1300
+ function normalizeHttpUrl$1(url) {
1301
+ const trimmed = url?.trim();
1302
+ if (!trimmed) {
1303
+ return void 0;
1304
+ }
1305
+ try {
1306
+ const parsed = new URL(trimmed);
1307
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString() : void 0;
1308
+ } catch {
1309
+ return void 0;
1310
+ }
1311
+ }
1312
+ function normalizeHostname(hostname) {
1313
+ return hostname.trim().toLowerCase().replace(/^\[|\]$/g, "");
1314
+ }
1315
+ function parseIpv4(hostname) {
1316
+ const parts = hostname.split(".");
1317
+ if (parts.length !== 4) {
1318
+ return null;
1319
+ }
1320
+ const octets = parts.map((part) => {
1321
+ if (!/^\d+$/.test(part)) {
1322
+ return Number.NaN;
1323
+ }
1324
+ return Number(part);
1325
+ });
1326
+ if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
1327
+ return null;
1328
+ }
1329
+ return octets;
1330
+ }
1331
+ function isPrivateIpv4(hostname) {
1332
+ const octets = parseIpv4(normalizeHostname(hostname));
1333
+ if (!octets) {
1334
+ return false;
1335
+ }
1336
+ const [a, b] = octets;
1337
+ return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a >= 224 && a <= 239 || a >= 240;
1338
+ }
1339
+ function isPrivateIpv6(hostname) {
1340
+ const normalized = normalizeHostname(hostname);
1341
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
1342
+ }
1343
+ function isUnsafeHostname(hostname) {
1344
+ const normalized = normalizeHostname(hostname);
1345
+ if (normalized === "localhost" || normalized.endsWith(".localhost")) {
1346
+ return true;
1347
+ }
1348
+ const ipVersion = isIP(normalized);
1349
+ if (ipVersion === 4) {
1350
+ return isPrivateIpv4(normalized);
1351
+ }
1352
+ if (ipVersion === 6) {
1353
+ return isPrivateIpv6(normalized);
1354
+ }
1355
+ return false;
1356
+ }
1357
+ function isPublicDiscoveryBase(url) {
1358
+ try {
1359
+ const parsed = new URL(url);
1360
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.hostname === "github.com" || parseGitHubRepo(url) || isUnsafeHostname(parsed.hostname)) {
1361
+ return false;
1362
+ }
1363
+ return true;
1364
+ } catch {
1365
+ return false;
1366
+ }
1367
+ }
1368
+ function isSameOriginUrl(url, baseUrl) {
1369
+ return new URL(url).origin === new URL(baseUrl).origin;
1370
+ }
1371
+ function resolveSameOriginUrl(rawUrl, baseUrl) {
1372
+ const trimmed = rawUrl.trim();
1373
+ if (!trimmed) {
1374
+ return null;
1375
+ }
1376
+ try {
1377
+ const resolved = new URL(trimmed, baseUrl);
1378
+ if (!isSameOriginUrl(resolved.toString(), baseUrl) || isUnsafeHostname(resolved.hostname)) {
1379
+ return null;
1380
+ }
1381
+ return resolved.toString();
1382
+ } catch {
1383
+ return null;
1384
+ }
1385
+ }
1386
+ function docsBaseCandidates(packageInfo) {
1387
+ const override = findPackageOverride(packageInfo.packageName);
1388
+ const candidates = [
1389
+ ...override?.docsUrls || [],
1390
+ packageInfo.homepage || ""
1391
+ ];
1392
+ return [...new Set(candidates.map(normalizeHttpUrl$1).filter((url) => Boolean(url)).filter(isPublicDiscoveryBase))];
1393
+ }
1394
+ function normalizeDigest(value) {
1395
+ if (typeof value !== "string") {
1396
+ return null;
1397
+ }
1398
+ const trimmed = value.trim();
1399
+ return /^sha256:[a-f0-9]{64}$/.test(trimmed) ? trimmed : null;
1400
+ }
1401
+ function sha256(bytes) {
1402
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
1403
+ }
1404
+ async function writeSkillFile(root, relativePath, bytes) {
1405
+ const destination = join(root, relativePath);
1406
+ await ensureDir(dirname(destination));
1407
+ await promises.writeFile(destination, bytes);
1408
+ }
1409
+ function contributionFor(input) {
1410
+ const sourceRepo = parseGitHubRepo(input.packageInfo.repository);
1411
+ return normalizeContribution({
1412
+ packageName: input.packageInfo.packageName,
1413
+ version: input.packageInfo.version,
1414
+ skillName: input.skillName,
1415
+ description: input.description,
1416
+ sourceKind: "wellKnown",
1417
+ sourceRepo: sourceRepo || void 0,
1418
+ sourcePath: input.indexUrl,
1419
+ repoUrl: sourceRepo ? `https://github.com/${sourceRepo}` : void 0,
1420
+ docsUrl: input.docsUrl,
1421
+ official: true,
1422
+ resolver: input.resolver,
1423
+ forceIncludeScripts: true
1424
+ }, input.targetDir, join(input.targetDir, ".."));
1425
+ }
1426
+ async function materializeV2Skill(input) {
1427
+ const skillName = typeof input.entry.name === "string" ? input.entry.name.trim() : "";
1428
+ if (!isValidSkillName(skillName)) {
1429
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName || "entry", "well-known v2 skill name is invalid") };
1430
+ }
1431
+ const description = typeof input.entry.description === "string" ? input.entry.description.trim() : "";
1432
+ if (!description) {
1433
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill is missing a description") };
1434
+ }
1435
+ const type = typeof input.entry.type === "string" ? input.entry.type.trim() : "";
1436
+ if (type === "archive") {
1437
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "archive artifacts are not supported yet") };
1438
+ }
1439
+ if (type !== "skill-md") {
1440
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, `unsupported well-known v2 skill type "${type || "unknown"}"`) };
1441
+ }
1442
+ if (typeof input.entry.url !== "string" || !input.entry.url.trim()) {
1443
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill is missing a URL") };
1444
+ }
1445
+ const digest = normalizeDigest(input.entry.digest);
1446
+ if (!digest) {
1447
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill is missing a valid sha256 digest") };
1448
+ }
1449
+ const skillUrl = resolveSameOriginUrl(input.entry.url, input.indexUrl);
1450
+ if (!skillUrl) {
1451
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill URL must stay on the discovery origin") };
1452
+ }
1453
+ const response = await fetchUrlBytes(skillUrl, input.timeoutMs);
1454
+ if (!response.ok || !response.data) {
1455
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "failed to fetch well-known v2 skill artifact") };
1456
+ }
1457
+ if (sha256(response.data) !== digest) {
1458
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill digest mismatch") };
1459
+ }
1460
+ const origin = new URL(input.docsUrl).origin;
1461
+ const targetDir = join(
1462
+ input.cacheRoot,
1463
+ "well-known",
1464
+ "rfc",
1465
+ sanitizeSegment(origin),
1466
+ sanitizeSegment(input.packageInfo.packageName),
1467
+ sanitizeSegment(input.packageInfo.version || "unknown"),
1468
+ sanitizeSegment(skillName)
1469
+ );
1470
+ await emptyDir(targetDir);
1471
+ await writeSkillFile(targetDir, "SKILL.md", response.data);
1472
+ return {
1473
+ contribution: contributionFor({
1474
+ packageInfo: input.packageInfo,
1475
+ targetDir,
1476
+ skillName,
1477
+ description,
1478
+ docsUrl: input.docsUrl,
1479
+ resolver: "wellKnownV2",
1480
+ indexUrl: input.indexUrl
1481
+ })
1482
+ };
1483
+ }
1484
+ async function resolveV2Index(input) {
1485
+ const skipped = [];
1486
+ const contributions = [];
1487
+ if (!Array.isArray(input.index.skills)) {
1488
+ return {
1489
+ contributions: [],
1490
+ issues: [],
1491
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known v2 index is missing a skills array")]
1492
+ };
1493
+ }
1494
+ for (const rawEntry of input.index.skills) {
1495
+ if (!rawEntry || typeof rawEntry !== "object") {
1496
+ skipped.push(makeSkip$1(input.packageInfo.packageName, "entry", "well-known v2 skill entry must be an object"));
1497
+ continue;
1498
+ }
1499
+ const materialized = await materializeV2Skill({
1500
+ packageInfo: input.packageInfo,
1501
+ entry: rawEntry,
1502
+ indexUrl: input.indexUrl,
1503
+ docsUrl: input.docsUrl,
1504
+ cacheRoot: input.cacheRoot,
1505
+ timeoutMs: input.timeoutMs
1506
+ });
1507
+ if (materialized?.contribution) {
1508
+ contributions.push(materialized.contribution);
1509
+ }
1510
+ if (materialized?.skipped) {
1511
+ skipped.push(materialized.skipped);
1512
+ }
1513
+ }
1514
+ return { contributions, issues: [], skipped };
1515
+ }
1516
+ async function resolveIndex(input) {
1517
+ const response = await fetchUrlJson(input.indexUrl, input.timeoutMs);
1518
+ if (!response.ok) {
1519
+ if (response.status === 404) {
1520
+ return null;
1521
+ }
1522
+ return {
1523
+ contributions: [],
1524
+ issues: [],
1525
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, `failed to fetch well-known index ${input.indexUrl}`)]
1526
+ };
1527
+ }
1528
+ const index = response.data;
1529
+ if (!index || typeof index !== "object") {
1530
+ return {
1531
+ contributions: [],
1532
+ issues: [],
1533
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known index must be an object")]
1534
+ };
1535
+ }
1536
+ if (index.$schema === WELL_KNOWN_DISCOVERY_V2_SCHEMA) {
1537
+ return await resolveV2Index({
1538
+ ...input,
1539
+ index
1540
+ });
1541
+ }
1542
+ if (typeof index.$schema === "string" && index.$schema) {
1543
+ return {
1544
+ contributions: [],
1545
+ issues: [],
1546
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, `unsupported well-known schema "${index.$schema}"`)]
1547
+ };
1548
+ }
1549
+ return {
1550
+ contributions: [],
1551
+ issues: [],
1552
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known index is missing the v2 schema")]
1553
+ };
1554
+ }
1555
+ async function resolveViaWellKnown(packageInfo, cacheRoot, timeoutMs) {
1556
+ const bases = docsBaseCandidates(packageInfo);
1557
+ const skipped = [];
1558
+ const issues = [];
1559
+ for (const docsUrl of bases) {
1560
+ const indexUrl = new URL(WELL_KNOWN_DISCOVERY_V2_INDEX_PATH, docsUrl).toString();
1561
+ const result = await resolveIndex({
1562
+ packageInfo,
1563
+ indexUrl,
1564
+ docsUrl,
1565
+ cacheRoot,
1566
+ timeoutMs
1567
+ });
1568
+ if (!result) {
1569
+ continue;
1570
+ }
1571
+ issues.push(...result.issues);
1572
+ skipped.push(...result.skipped);
1573
+ if (result.contributions.length) {
1574
+ return {
1575
+ contributions: result.contributions,
1576
+ issues,
1577
+ skipped
1578
+ };
1579
+ }
1580
+ }
1581
+ if (!bases.length) {
1582
+ return {
1583
+ contributions: [],
1584
+ issues: [],
1585
+ skipped: []
1586
+ };
1587
+ }
1588
+ return {
1589
+ contributions: [],
1590
+ issues,
1591
+ skipped
1592
+ };
1593
+ }
1174
1594
 
1175
1595
  function makeSkip(packageName, skillName, reason, sourceKind) {
1176
1596
  return {
@@ -1213,8 +1633,14 @@ function normalizeHttpUrl(url) {
1213
1633
  return void 0;
1214
1634
  }
1215
1635
  }
1636
+ function resolveDocsUrl(packageInfo) {
1637
+ const override = findPackageOverride(packageInfo.packageName);
1638
+ const overrideDocsUrl = override?.docsUrls?.map(normalizeHttpUrl).find(Boolean);
1639
+ return overrideDocsUrl || normalizeHttpUrl(packageInfo.homepage);
1640
+ }
1216
1641
  function resolveRepositoryUrl(packageInfo) {
1217
- const sourceRepo = parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
1642
+ const override = findPackageOverride(packageInfo.packageName);
1643
+ const sourceRepo = override?.repo || parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
1218
1644
  if (sourceRepo) {
1219
1645
  return {
1220
1646
  sourceRepo,
@@ -1225,6 +1651,15 @@ function resolveRepositoryUrl(packageInfo) {
1225
1651
  repoUrl: normalizeHttpUrl(packageInfo.repository)
1226
1652
  };
1227
1653
  }
1654
+ function isImmutableRef(ref, version) {
1655
+ if (!ref) {
1656
+ return false;
1657
+ }
1658
+ if (version && (ref === version || ref === `v${version}`)) {
1659
+ return true;
1660
+ }
1661
+ return /^[a-f0-9]{7,40}$/i.test(ref);
1662
+ }
1228
1663
  async function materializeCandidate(packageInfo, candidate, cacheRoot) {
1229
1664
  const targetDir = join(
1230
1665
  cacheRoot,
@@ -1232,8 +1667,24 @@ async function materializeCandidate(packageInfo, candidate, cacheRoot) {
1232
1667
  sanitizeSegment(candidate.sourceRepo),
1233
1668
  sanitizeSegment(candidate.sourceRef),
1234
1669
  sanitizeSegment(packageInfo.packageName),
1670
+ sanitizeSegment(packageInfo.version || "unknown"),
1235
1671
  sanitizeSegment(candidate.skillName)
1236
1672
  );
1673
+ const skillFilePath = join(targetDir, "SKILL.md");
1674
+ if (isImmutableRef(candidate.sourceRef, packageInfo.version) && await pathExists(skillFilePath)) {
1675
+ return normalizeContribution({
1676
+ packageName: packageInfo.packageName,
1677
+ version: packageInfo.version,
1678
+ skillName: candidate.skillName,
1679
+ sourceKind: candidate.sourceKind,
1680
+ sourceRepo: candidate.sourceRepo,
1681
+ sourceRef: candidate.sourceRef,
1682
+ sourcePath: candidate.sourcePath,
1683
+ official: candidate.official,
1684
+ resolver: candidate.resolver,
1685
+ forceIncludeScripts: true
1686
+ }, targetDir, join(targetDir, ".."));
1687
+ }
1237
1688
  try {
1238
1689
  await downloadTemplate(`gh:${candidate.sourceRepo}/${candidate.sourcePath}#${candidate.sourceRef}`, {
1239
1690
  dir: targetDir,
@@ -1245,7 +1696,7 @@ async function materializeCandidate(packageInfo, candidate, cacheRoot) {
1245
1696
  } catch {
1246
1697
  return null;
1247
1698
  }
1248
- if (!await pathExists(join(targetDir, "SKILL.md"))) {
1699
+ if (!await pathExists(skillFilePath)) {
1249
1700
  return null;
1250
1701
  }
1251
1702
  return normalizeContribution({
@@ -1261,25 +1712,13 @@ async function materializeCandidate(packageInfo, candidate, cacheRoot) {
1261
1712
  forceIncludeScripts: true
1262
1713
  }, targetDir, join(targetDir, ".."));
1263
1714
  }
1264
- async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1265
- const issues = [];
1266
- const skipped = [];
1267
- const override = findGitHubOverride(packageInfo.packageName);
1715
+ function createGitHubResolveContext(packageInfo) {
1716
+ const override = findPackageOverride(packageInfo.packageName);
1268
1717
  const repo = override?.repo || parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
1269
- if (!repo) {
1270
- return {
1271
- contributions: [],
1272
- issues,
1273
- skipped: [makeSkip(packageInfo.packageName, override?.skillName || packageInfo.packageName, "No GitHub repository metadata found", "github")]
1274
- };
1275
- }
1276
1718
  const refs = dedupe([
1277
1719
  override?.ref || "",
1278
1720
  packageInfo.version ? `v${packageInfo.version}` : "",
1279
- packageInfo.version || "",
1280
- await fetchGitHubDefaultBranch(repo, timeoutMs) || "",
1281
- "main",
1282
- "master"
1721
+ packageInfo.version || ""
1283
1722
  ]);
1284
1723
  const candidates = dedupe([
1285
1724
  override?.skillName || "",
@@ -1295,32 +1734,89 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1295
1734
  ];
1296
1735
  })
1297
1736
  ]);
1298
- for (const ref of refs) {
1299
- const packageJson = await fetchGitHubFileText(repo, ref, "package.json", timeoutMs);
1300
- if (packageJson.ok && packageJson.data) {
1301
- try {
1302
- const parsed = JSON.parse(packageJson.data);
1303
- const remoteSkills = parseAgentSkillDeclarations(parsed, packageInfo.packageName, "github");
1304
- issues.push(...remoteSkills.issues);
1305
- for (const skill of remoteSkills.skills) {
1306
- const resolved = await materializeCandidate(packageInfo, {
1307
- skillName: skill.name,
1308
- sourcePath: skill.path,
1309
- sourceKind: "github",
1310
- sourceRepo: repo,
1311
- sourceRef: ref,
1312
- official: true,
1313
- resolver: "agentsField"
1314
- }, cacheRoot);
1315
- if (resolved) {
1316
- return { contributions: [resolved], issues, skipped };
1317
- }
1318
- }
1319
- } catch {
1320
- issues.push(createValidationIssue(packageInfo.packageName, packageInfo.packageName, "Failed to parse remote package.json", "github"));
1737
+ return {
1738
+ issues: [],
1739
+ skipped: [],
1740
+ override,
1741
+ repo: repo || void 0,
1742
+ refs,
1743
+ pathCandidates
1744
+ };
1745
+ }
1746
+ async function resolveAgentSkillsForRef(packageInfo, context, ref, cacheRoot, timeoutMs) {
1747
+ if (!context.repo) {
1748
+ return null;
1749
+ }
1750
+ const packageJson = await fetchGitHubFileText(context.repo, ref, "package.json", timeoutMs);
1751
+ if (!packageJson.ok || !packageJson.data) {
1752
+ return null;
1753
+ }
1754
+ try {
1755
+ const parsed = JSON.parse(packageJson.data);
1756
+ const remoteSkills = parseAgentSkillDeclarations(parsed, packageInfo.packageName, "github");
1757
+ context.issues.push(...remoteSkills.issues);
1758
+ for (const skill of remoteSkills.skills) {
1759
+ const resolved = await materializeCandidate(packageInfo, {
1760
+ skillName: skill.name,
1761
+ sourcePath: skill.path,
1762
+ sourceKind: "github",
1763
+ sourceRepo: context.repo,
1764
+ sourceRef: ref,
1765
+ official: true,
1766
+ resolver: "agentsField"
1767
+ }, cacheRoot);
1768
+ if (resolved) {
1769
+ return [resolved];
1321
1770
  }
1322
1771
  }
1323
- const skillsDirs = await listGitHubDirectory(repo, ref, "skills", timeoutMs);
1772
+ } catch {
1773
+ context.issues.push(createValidationIssue(packageInfo.packageName, packageInfo.packageName, "Failed to parse remote package.json", "github"));
1774
+ }
1775
+ return null;
1776
+ }
1777
+ async function resolveViaGitHubAgents(packageInfo, cacheRoot, timeoutMs) {
1778
+ const context = createGitHubResolveContext(packageInfo);
1779
+ if (!context.repo) {
1780
+ return {
1781
+ contributions: [],
1782
+ issues: context.issues,
1783
+ skipped: []
1784
+ };
1785
+ }
1786
+ for (const ref of context.refs) {
1787
+ const resolved = await resolveAgentSkillsForRef(packageInfo, context, ref, cacheRoot, timeoutMs);
1788
+ if (resolved?.length) {
1789
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
1790
+ }
1791
+ }
1792
+ const fallbackRefs = dedupe([
1793
+ await fetchGitHubDefaultBranch(context.repo, timeoutMs) || "",
1794
+ "main",
1795
+ "master"
1796
+ ]);
1797
+ for (const ref of fallbackRefs) {
1798
+ const resolved = await resolveAgentSkillsForRef(packageInfo, context, ref, cacheRoot, timeoutMs);
1799
+ if (resolved?.length) {
1800
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
1801
+ }
1802
+ }
1803
+ return {
1804
+ contributions: [],
1805
+ issues: context.issues,
1806
+ skipped: context.skipped
1807
+ };
1808
+ }
1809
+ async function resolveViaGitHubHeuristics(packageInfo, cacheRoot, timeoutMs) {
1810
+ const context = createGitHubResolveContext(packageInfo);
1811
+ if (!context.repo) {
1812
+ return {
1813
+ contributions: [],
1814
+ issues: context.issues,
1815
+ skipped: [makeSkip(packageInfo.packageName, context.override?.skillName || packageInfo.packageName, "No GitHub repository metadata found", "github")]
1816
+ };
1817
+ }
1818
+ const tryResolveForRef = async (ref, allowTreeLookup) => {
1819
+ const skillsDirs = await listGitHubDirectory(context.repo, ref, "skills", timeoutMs) ;
1324
1820
  if (skillsDirs.length) {
1325
1821
  const contributions = [];
1326
1822
  for (const skillName of skillsDirs) {
@@ -1328,7 +1824,7 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1328
1824
  skillName,
1329
1825
  sourcePath: `skills/${skillName}`,
1330
1826
  sourceKind: "github",
1331
- sourceRepo: repo,
1827
+ sourceRepo: context.repo,
1332
1828
  sourceRef: ref,
1333
1829
  official: true,
1334
1830
  resolver: "githubHeuristic"
@@ -1338,35 +1834,53 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1338
1834
  }
1339
1835
  }
1340
1836
  if (contributions.length) {
1341
- return { contributions, issues, skipped };
1837
+ return contributions;
1342
1838
  }
1343
1839
  }
1344
- for (const path of pathCandidates) {
1345
- const candidateSkillName = override?.path && path === override.path ? override.skillName || path.split("/").pop() || packageInfo.packageName : path.split("/").pop() || packageInfo.packageName;
1840
+ for (const path of context.pathCandidates) {
1841
+ const candidateSkillName = context.override?.path && path === context.override.path ? context.override.skillName || path.split("/").pop() || packageInfo.packageName : path.split("/").pop() || packageInfo.packageName;
1346
1842
  const resolved = await materializeCandidate(packageInfo, {
1347
1843
  skillName: candidateSkillName,
1348
1844
  sourcePath: path,
1349
1845
  sourceKind: "github",
1350
- sourceRepo: repo,
1846
+ sourceRepo: context.repo,
1351
1847
  sourceRef: ref,
1352
1848
  official: true,
1353
1849
  resolver: "githubHeuristic"
1354
1850
  }, cacheRoot);
1355
1851
  if (resolved) {
1356
- return { contributions: [resolved], issues, skipped };
1852
+ return [resolved];
1357
1853
  }
1358
1854
  }
1855
+ return null;
1856
+ };
1857
+ const fallbackRefs = dedupe([
1858
+ await fetchGitHubDefaultBranch(context.repo, timeoutMs) || "",
1859
+ "main",
1860
+ "master"
1861
+ ]);
1862
+ for (const ref of fallbackRefs) {
1863
+ const resolved = await tryResolveForRef(ref);
1864
+ if (resolved?.length) {
1865
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
1866
+ }
1867
+ }
1868
+ for (const ref of context.refs) {
1869
+ const resolved = await tryResolveForRef(ref);
1870
+ if (resolved?.length) {
1871
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
1872
+ }
1359
1873
  }
1360
- skipped.push(makeSkip(packageInfo.packageName, override?.skillName || packageInfo.packageName, "No GitHub skill found using configured refs and path heuristics", "github"));
1874
+ context.skipped.push(makeSkip(packageInfo.packageName, context.override?.skillName || packageInfo.packageName, "No GitHub skill found using configured refs and path heuristics", "github"));
1361
1875
  return {
1362
1876
  contributions: [],
1363
- issues,
1364
- skipped
1877
+ issues: context.issues,
1878
+ skipped: context.skipped
1365
1879
  };
1366
1880
  }
1367
1881
  async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
1368
1882
  const { repoUrl, sourceRepo } = resolveRepositoryUrl(packageInfo);
1369
- const docsUrl = normalizeHttpUrl(packageInfo.homepage);
1883
+ const docsUrl = resolveDocsUrl(packageInfo);
1370
1884
  if (!repoUrl && !docsUrl) {
1371
1885
  return {
1372
1886
  contributions: [],
@@ -1380,8 +1894,31 @@ async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
1380
1894
  "generated",
1381
1895
  "metadata-router",
1382
1896
  sanitizeSegment(packageInfo.packageName),
1897
+ sanitizeSegment(packageInfo.version || "unknown"),
1383
1898
  sanitizeSegment(skillName)
1384
1899
  );
1900
+ const skillFilePath = join(targetDir, "SKILL.md");
1901
+ if (await pathExists(skillFilePath)) {
1902
+ return {
1903
+ contributions: [
1904
+ normalizeContribution({
1905
+ packageName: packageInfo.packageName,
1906
+ version: packageInfo.version,
1907
+ skillName,
1908
+ description: packageInfo.description,
1909
+ sourceKind: "generated",
1910
+ sourceRepo,
1911
+ repoUrl,
1912
+ docsUrl,
1913
+ official: true,
1914
+ resolver: "metadataRouter",
1915
+ forceIncludeScripts: false
1916
+ }, targetDir, join(targetDir, ".."))
1917
+ ],
1918
+ issues: [],
1919
+ skipped: []
1920
+ };
1921
+ }
1385
1922
  const files = createMetadataRouterSkillFiles({
1386
1923
  packageName: packageInfo.packageName,
1387
1924
  skillName,
@@ -1415,26 +1952,46 @@ async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
1415
1952
  };
1416
1953
  }
1417
1954
  async function resolveRemoteContributionsForPackage(packageInfo, options) {
1418
- const githubResult = options.enableGithubLookup ? await resolveViaGitHub(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
1955
+ const githubAgentsResult = options.enableGithubLookup ? await resolveViaGitHubAgents(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
1419
1956
  contributions: [],
1420
1957
  issues: [],
1421
1958
  skipped: []
1422
1959
  };
1423
- if (githubResult.contributions.length) {
1424
- return githubResult;
1960
+ if (githubAgentsResult.contributions.length) {
1961
+ return githubAgentsResult;
1962
+ }
1963
+ const wellKnownResult = await resolveViaWellKnown(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs);
1964
+ if (wellKnownResult.contributions.length) {
1965
+ return {
1966
+ contributions: wellKnownResult.contributions,
1967
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues],
1968
+ skipped: wellKnownResult.skipped
1969
+ };
1970
+ }
1971
+ const githubHeuristicResult = options.enableGithubLookup ? await resolveViaGitHubHeuristics(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
1972
+ contributions: [],
1973
+ issues: [],
1974
+ skipped: []
1975
+ };
1976
+ if (githubHeuristicResult.contributions.length) {
1977
+ return {
1978
+ contributions: githubHeuristicResult.contributions,
1979
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues, ...githubHeuristicResult.issues],
1980
+ skipped: wellKnownResult.skipped
1981
+ };
1425
1982
  }
1426
1983
  const generatedResult = await resolveViaMetadataRouter(packageInfo, options.cacheRoot);
1427
1984
  if (generatedResult.contributions.length) {
1428
1985
  return {
1429
1986
  contributions: generatedResult.contributions,
1430
- issues: [...githubResult.issues, ...generatedResult.issues],
1431
- skipped: generatedResult.skipped
1987
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues, ...githubHeuristicResult.issues, ...generatedResult.issues],
1988
+ skipped: [...wellKnownResult.skipped, ...generatedResult.skipped]
1432
1989
  };
1433
1990
  }
1434
1991
  return {
1435
1992
  contributions: [],
1436
- issues: [...githubResult.issues, ...generatedResult.issues],
1437
- skipped: [...githubResult.skipped, ...generatedResult.skipped]
1993
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues, ...githubHeuristicResult.issues, ...generatedResult.issues],
1994
+ skipped: [...wellKnownResult.skipped, ...githubHeuristicResult.skipped, ...generatedResult.skipped]
1438
1995
  };
1439
1996
  }
1440
1997
 
@@ -1492,6 +2049,7 @@ async function hashFileIfExists(path) {
1492
2049
  }
1493
2050
  }
1494
2051
 
2052
+ const HASH_READ_CONCURRENCY = 16;
1495
2053
  async function resolveManualContribution(rootDir, contribution) {
1496
2054
  const sourceDir = isAbsolute(contribution.sourceDir) ? contribution.sourceDir : resolve(rootDir, contribution.sourceDir);
1497
2055
  return normalizeContribution(contribution, sourceDir, sourceDir);
@@ -1543,8 +2101,6 @@ async function hashDirectoryTreeIfExists(rootPath) {
1543
2101
  if (!rootStat.isDirectory()) {
1544
2102
  return null;
1545
2103
  }
1546
- const hash = createHash("sha256");
1547
- hash.update("dir:.\n");
1548
2104
  const entries = await glob("**/*", {
1549
2105
  cwd: rootPath,
1550
2106
  dot: true,
@@ -1553,30 +2109,20 @@ async function hashDirectoryTreeIfExists(rootPath) {
1553
2109
  onlyFiles: false
1554
2110
  });
1555
2111
  entries.sort((a, b) => a.localeCompare(b));
1556
- for (const entry of entries) {
2112
+ const chunkGroups = await mapWithConcurrency(entries, HASH_READ_CONCURRENCY, async (entry) => {
1557
2113
  const currentPath = join(rootPath, entry);
1558
2114
  const stat = await promises.lstat(currentPath);
1559
- const relativePath = relative(rootPath, currentPath).replaceAll("\\", "/");
1560
- if (stat.isSymbolicLink()) {
1561
- hash.update(`symlink:${relativePath}:`);
1562
- hash.update(await promises.readlink(currentPath));
1563
- hash.update("\n");
1564
- continue;
1565
- }
1566
- if (stat.isDirectory()) {
1567
- hash.update(`dir:${relativePath}
1568
- `);
1569
- continue;
1570
- }
1571
- if (stat.isFile()) {
1572
- hash.update(`file:${relativePath}:`);
1573
- hash.update(await promises.readFile(currentPath));
1574
- hash.update("\n");
1575
- continue;
1576
- }
1577
- hash.update(`other:${relativePath}:${stat.mode}
1578
- `);
1579
- }
2115
+ const rel = relative(rootPath, currentPath).replaceAll("\\", "/");
2116
+ if (stat.isSymbolicLink()) return [`symlink:${rel}:`, await promises.readlink(currentPath), "\n"];
2117
+ if (stat.isDirectory()) return [`dir:${rel}
2118
+ `];
2119
+ if (stat.isFile()) return [`file:${rel}:`, await promises.readFile(currentPath), "\n"];
2120
+ return [`other:${rel}:${stat.mode}
2121
+ `];
2122
+ });
2123
+ const hash = createHash("sha256");
2124
+ hash.update("dir:.\n");
2125
+ for (const chunks of chunkGroups) for (const chunk of chunks) hash.update(chunk);
1580
2126
  return hash.digest("hex");
1581
2127
  }
1582
2128
 
@@ -1613,6 +2159,7 @@ async function isGeneratedSkillFresh(generatedSkillRoot, fingerprint) {
1613
2159
  }
1614
2160
 
1615
2161
  const GITHUB_LOOKUP_TIMEOUT_MS = 1500;
2162
+ const REMOTE_RESOLUTION_CONCURRENCY = 4;
1616
2163
  function resolveStableSkillBuildDir(rootDir, buildDir) {
1617
2164
  const rel = relative(resolve(rootDir), resolve(buildDir)).replaceAll("\\", "/");
1618
2165
  if (rel === ".nuxt" || rel.startsWith(".nuxt/")) {
@@ -1630,6 +2177,12 @@ function normalizeRelativePath(from, to) {
1630
2177
  function getGeneratedSkillRoot(buildDir, skillName) {
1631
2178
  return join(buildDir, "skill-hub", skillName);
1632
2179
  }
2180
+ function getPersistentCacheRoot(exportRoot) {
2181
+ return join(exportRoot, "node_modules", ".cache", "nuxt-skill-hub");
2182
+ }
2183
+ function getCachedGeneratedSkillRoot(cacheRoot, skillName) {
2184
+ return join(cacheRoot, "generated", skillName);
2185
+ }
1633
2186
  function mergeSkipped(entries, keyFn) {
1634
2187
  const byKey = /* @__PURE__ */ new Map();
1635
2188
  for (const entry of entries) {
@@ -1666,17 +2219,13 @@ async function collectInstalledPackages(nuxt) {
1666
2219
  discoveries.push(discovered);
1667
2220
  }
1668
2221
  };
1669
- const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
1670
- const seenSpecifiers = Array.from(new Set(moduleSpecifiers));
1671
- for (const specifier of seenSpecifiers) {
1672
- addInstalledPackage(await discoverInstalledPackageFromSpecifier(specifier, nuxt.options.rootDir));
1673
- }
2222
+ const modulePackages = await collectInstalledModulePackages(nuxt.options.modules, nuxt.options.rootDir);
2223
+ for (const pkg of modulePackages) addInstalledPackage(pkg);
1674
2224
  const layerDirectories = Array.from(new Set(
1675
2225
  nuxt.options._layers.map((layer) => resolve(layer.cwd || layer.config.rootDir || "")).filter((layerDir) => layerDir && layerDir !== resolve(nuxt.options.rootDir))
1676
2226
  ));
1677
- for (const layerDirectory of layerDirectories) {
1678
- addInstalledPackage(await discoverInstalledPackageFromDirectory(layerDirectory));
1679
- }
2227
+ const layerPackages = await Promise.all(layerDirectories.map((dir) => discoverInstalledPackageFromDirectory(dir)));
2228
+ for (const pkg of layerPackages) addInstalledPackage(pkg);
1680
2229
  const localPackageSkills = await discoverLocalPackageSkills(nuxt.options.rootDir);
1681
2230
  if (localPackageSkills) {
1682
2231
  discoveries.push(localPackageSkills);
@@ -1706,6 +2255,8 @@ async function ensureStableSkillWrappers(input) {
1706
2255
  async function generateSkillTree(input) {
1707
2256
  const { nuxt, logger, options, skillName, exportRoot, generationMode } = input;
1708
2257
  const stableBuildDir = resolveStableSkillBuildDir(nuxt.options.rootDir, nuxt.options.buildDir);
2258
+ const persistentCacheRoot = getPersistentCacheRoot(exportRoot);
2259
+ const cachedGeneratedSkillRoot = getCachedGeneratedSkillRoot(persistentCacheRoot, skillName);
1709
2260
  const generatedSkillRoot = getGeneratedSkillRoot(stableBuildDir, skillName);
1710
2261
  const referencesRoot = join(generatedSkillRoot, "references");
1711
2262
  const nuxtRoot = join(referencesRoot, "nuxt");
@@ -1739,26 +2290,32 @@ async function generateSkillTree(input) {
1739
2290
  ...sortAndDedupeContributions(resolvedManual)
1740
2291
  ])
1741
2292
  });
1742
- if (generationMode === "prepare" && await isGeneratedSkillFresh(generatedSkillRoot, fingerprint)) {
1743
- logger.info(`Generated ${skillName} skill is already up to date at ${generatedSkillRoot}`);
2293
+ if (generationMode === "prepare" && await isGeneratedSkillFresh(cachedGeneratedSkillRoot, fingerprint)) {
2294
+ await copySkillTree(cachedGeneratedSkillRoot, generatedSkillRoot, true);
2295
+ logger.info(`Restored cached ${skillName} skill from ${cachedGeneratedSkillRoot}`);
1744
2296
  return;
1745
2297
  }
1746
2298
  const distResolvedPackages = new Set(discoveredContributions.contributions.map((item) => item.packageName));
1747
2299
  const remoteIssues = [];
1748
2300
  const remoteSkipped = [];
1749
2301
  const remoteContributions = [];
1750
- const remoteCacheRoot = join(stableBuildDir, "skill-hub-cache");
1751
- await emptyDir(remoteCacheRoot);
2302
+ const remoteCacheRoot = join(persistentCacheRoot, "remote");
2303
+ await ensureDir(remoteCacheRoot);
1752
2304
  const packagesToResolve = installedPackages.filter((pkg) => pkg.packageName !== "nuxt-skill-hub" && !distResolvedPackages.has(pkg.packageName));
1753
2305
  const totalToResolve = packagesToResolve.length;
1754
- for (let i = 0; i < packagesToResolve.length; i++) {
1755
- const pkg = packagesToResolve[i];
1756
- logger.start(`[${i + 1}/${totalToResolve}] Resolving skills for ${pkg.packageName}...`);
1757
- const remote = await resolveRemoteContributionsForPackage(pkg, {
1758
- cacheRoot: remoteCacheRoot,
1759
- githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
1760
- enableGithubLookup: true
1761
- });
2306
+ const remoteResults = await mapWithConcurrency(
2307
+ packagesToResolve,
2308
+ REMOTE_RESOLUTION_CONCURRENCY,
2309
+ async (pkg, index) => {
2310
+ logger.start(`Resolving skills for ${pkg.packageName} (${index + 1}/${totalToResolve})...`);
2311
+ return await resolveRemoteContributionsForPackage(pkg, {
2312
+ cacheRoot: remoteCacheRoot,
2313
+ githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
2314
+ enableGithubLookup: true
2315
+ });
2316
+ }
2317
+ );
2318
+ for (const remote of remoteResults) {
1762
2319
  remoteIssues.push(...remote.issues);
1763
2320
  remoteSkipped.push(...remote.skipped);
1764
2321
  remoteContributions.push(...remote.contributions);
@@ -1841,14 +2398,47 @@ async function generateSkillTree(input) {
1841
2398
  skipped
1842
2399
  ));
1843
2400
  await writeGenerationState(generatedSkillRoot, fingerprint);
2401
+ await copySkillTree(generatedSkillRoot, cachedGeneratedSkillRoot, true);
1844
2402
  logger.success(`Generated ${skillName} skill at ${generatedSkillRoot}`);
1845
2403
  }
1846
2404
 
1847
2405
  function isCancelled(value) {
1848
2406
  return value === null || typeof value === "symbol";
1849
2407
  }
2408
+ function buildAIGuidance(currentTarget, detectedAgent) {
2409
+ const agentTargetMap = {
2410
+ codex: "codex",
2411
+ claude: "claude-code",
2412
+ gemini: "gemini-code-assist",
2413
+ replit: "replit-agent"
2414
+ };
2415
+ const target = currentTarget || (detectedAgent ? agentTargetMap[detectedAgent] : void 0);
2416
+ const sourceLabel = detectedAgent ? ` (${detectedAgent})` : "";
2417
+ const snippetLines = [
2418
+ "export default defineNuxtConfig({",
2419
+ " skillHub: {",
2420
+ ...target ? [` targets: ['${target}'],`] : [],
2421
+ ` generationMode: 'prepare',`,
2422
+ " },",
2423
+ "})"
2424
+ ];
2425
+ return {
2426
+ intro: `Install wizard skipped because nuxt-skill-hub was installed by an AI agent${sourceLabel}. Add this to your nuxt.config.ts:`,
2427
+ snippet: snippetLines.join("\n"),
2428
+ optionalFlags: target ? "Optional flags: set `skillHub.skillName`, enable `skillHub.moduleAuthoring`, or switch to `skillHub.generationMode: 'manual'` if needed." : "Optional flags: add `skillHub.targets` to pin an agent target, set `skillHub.skillName`, enable `skillHub.moduleAuthoring`, or switch to `skillHub.generationMode: 'manual'` if needed."
2429
+ };
2430
+ }
1850
2431
  async function runInstallWizard(nuxt) {
1851
2432
  const logger = useLogger("nuxt-skill-hub");
2433
+ if (isAgent) {
2434
+ const guidance = buildAIGuidance(detectCurrentTarget(), agent);
2435
+ logger.info(guidance.intro);
2436
+ logger.info(`\`\`\`ts
2437
+ ${guidance.snippet}
2438
+ \`\`\``);
2439
+ logger.info(guidance.optionalFlags);
2440
+ return;
2441
+ }
1852
2442
  if (process.env.CI || !process.stdout.isTTY) {
1853
2443
  logger.info("Non-interactive environment detected, skipping install wizard.");
1854
2444
  return;
@@ -1903,15 +2493,7 @@ Let's configure AI agent skills for your Nuxt project.`
1903
2493
  }
1904
2494
  }
1905
2495
  }
1906
- const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
1907
- const uniqueSpecifiers = Array.from(new Set(moduleSpecifiers));
1908
- const installedModulePackages = [];
1909
- for (const specifier of uniqueSpecifiers) {
1910
- const pkg = await discoverInstalledPackageFromSpecifier(specifier, rootDir);
1911
- if (!pkg || pkg.packageName === "nuxt-skill-hub")
1912
- continue;
1913
- installedModulePackages.push(pkg.packageName);
1914
- }
2496
+ const installedModulePackages = (await collectInstalledModulePackages(nuxt.options.modules, rootDir)).map((pkg) => pkg.packageName).filter((name) => name !== "nuxt-skill-hub");
1915
2497
  if (installedModulePackages.length) {
1916
2498
  consola.info(`Detected ${installedModulePackages.length} installed module package(s):`);
1917
2499
  for (const packageName of installedModulePackages) {
@@ -2005,6 +2587,49 @@ Configure \`skillHub.skillName\`, \`skillHub.targets\`, or set \`skillHub.genera
2005
2587
  );
2006
2588
  }
2007
2589
 
2590
+ function createAutoImportAddon(nuxt) {
2591
+ let unimport;
2592
+ let nitroUnimport;
2593
+ const hook = nuxt.hook;
2594
+ hook("imports:context", (context) => {
2595
+ unimport = context;
2596
+ });
2597
+ hook("nitro:init", (nitro) => {
2598
+ nitroUnimport = nitro.unimport;
2599
+ });
2600
+ hook("eslint:config:addons", (addons) => {
2601
+ addons.push({
2602
+ name: "skill-hub:no-redundant-import",
2603
+ async getConfigs() {
2604
+ const imports = [
2605
+ ...await unimport?.getImports() || [],
2606
+ ...await nitroUnimport?.getImports() || []
2607
+ ];
2608
+ const seen = /* @__PURE__ */ new Set();
2609
+ const entries = imports.filter((i) => {
2610
+ const key = `${i.as || i.name}:${i.from}:${i.type ? "type" : "value"}`;
2611
+ if (seen.has(key)) return false;
2612
+ seen.add(key);
2613
+ return true;
2614
+ }).map((i) => ({ name: i.name, ...i.as && i.as !== i.name ? { as: i.as } : {}, from: i.from, ...i.type ? { type: true } : {} })).sort((a, b) => a.from.localeCompare(b.from) || a.name.localeCompare(b.name));
2615
+ return {
2616
+ imports: [{ from: "nuxt-skill-hub/eslint-plugin", name: "default", as: "skillHubPlugin" }],
2617
+ configs: [
2618
+ [
2619
+ "{",
2620
+ ` name: 'skill-hub/no-redundant-import',`,
2621
+ ` plugins: { 'skill-hub': skillHubPlugin },`,
2622
+ ` settings: { 'skill-hub/autoImports': ${JSON.stringify(entries)} },`,
2623
+ ` rules: { 'skill-hub/no-redundant-import': 'warn' },`,
2624
+ "}"
2625
+ ].join("\n")
2626
+ ]
2627
+ };
2628
+ }
2629
+ });
2630
+ });
2631
+ }
2632
+
2008
2633
  const module$1 = defineNuxtModule({
2009
2634
  meta: {
2010
2635
  name: "nuxt-skill-hub",
@@ -2025,6 +2650,8 @@ const module$1 = defineNuxtModule({
2025
2650
  },
2026
2651
  async setup(options, nuxt) {
2027
2652
  const logger = useLogger("nuxt-skill-hub");
2653
+ if (options.eslint !== false)
2654
+ createAutoImportAddon(nuxt);
2028
2655
  if (isCI && !isTest) {
2029
2656
  logger.info("Skipping skill generation in CI");
2030
2657
  return;