nuxt-skill-hub 0.0.7 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useLogger, defineNuxtModule } from '@nuxt/kit';
2
2
  import { isAgent, agent, isCI, isTest } from 'std-env';
3
3
  import { promises, existsSync, lstatSync, writeFileSync, readFileSync } from 'node:fs';
4
+ import { performance } from 'node:perf_hooks';
4
5
  import { join, basename, resolve, dirname, relative, isAbsolute } from 'pathe';
5
6
  import { fileURLToPath } from 'mlly';
6
7
  import { homedir } from 'node:os';
@@ -8,15 +9,16 @@ import matter from 'gray-matter';
8
9
  import { readPackageJSON, findWorkspaceDir, resolvePackageJSON, resolveLockfile } from 'pkg-types';
9
10
  import { transform } from 'automd';
10
11
  import { downloadTemplate } from 'giget';
11
- import { detectInstalledAgents, expandPath, detectCurrentAgent, getAgentIds, getAgentConfig } from 'unagent/env';
12
+ import { getAgentConfig, detectInstalledAgents, expandPath, detectCurrentAgent, getAgentIds } from 'unagent/env';
12
13
  import { ofetch } from 'ofetch';
13
14
  import { createHash } from 'node:crypto';
15
+ import { isIP } from 'node:net';
14
16
  import { hash } from 'ohash';
15
17
  import { glob } from 'tinyglobby';
16
18
  import { createConsola } from 'consola';
17
19
  import { colorize } from 'consola/utils';
18
20
 
19
- const version = "0.0.7";
21
+ const version = "0.1.0";
20
22
  const packageJson = {
21
23
  version: version};
22
24
 
@@ -146,6 +148,8 @@ function getSourceLabel(sourceKind) {
146
148
  return "Installed package skill";
147
149
  case "github":
148
150
  return "Resolved module skill";
151
+ case "wellKnown":
152
+ return "Docs-discovered skill";
149
153
  case "generated":
150
154
  return "Metadata-routed skill";
151
155
  }
@@ -175,14 +179,14 @@ function resolveMetadataRouterSkillName(packageName) {
175
179
  }
176
180
  function createMetadataRouterSkillFiles(input) {
177
181
  const description = input.description?.trim() || `Metadata-routed module skill for ${input.packageName}. Use it to reach the official docs and upstream source.`;
178
- const linkLines = createCompactMetadataRouterContent(input).trim();
182
+ const body = createMetadataRouterContent(input).trim();
179
183
  return {
180
184
  "SKILL.md": `---
181
185
  name: ${yamlString(input.skillName)}
182
186
  description: ${yamlString(description)}
183
187
  ---
184
188
 
185
- ${linkLines || "Docs: unavailable\nSource code: unavailable"}
189
+ ${body}
186
190
  `
187
191
  };
188
192
  }
@@ -206,8 +210,9 @@ function relativeModuleLink(entry, prefix) {
206
210
  function groupModuleEntries(entries) {
207
211
  const officialUpstream = entries.filter((entry) => entry.sourceKind === "dist");
208
212
  const githubResolved = entries.filter((entry) => entry.sourceKind === "github");
213
+ const wellKnownResolved = entries.filter((entry) => entry.sourceKind === "wellKnown");
209
214
  const metadataRouted = entries.filter((entry) => entry.sourceKind === "generated");
210
- return { officialUpstream, githubResolved, metadataRouted };
215
+ return { officialUpstream, githubResolved, wellKnownResolved, metadataRouted };
211
216
  }
212
217
  function hasModuleGuidance(entries, skipped = []) {
213
218
  return entries.length > 0 || skipped.length > 0;
@@ -241,6 +246,43 @@ function renderSkippedEntries(skipped) {
241
246
  ${lines.join("\n")}
242
247
  `;
243
248
  }
249
+ function renderModuleSummary(entries, skipped) {
250
+ if (!hasModuleGuidance(entries, skipped)) {
251
+ return "";
252
+ }
253
+ const grouped = groupModuleEntries(entries);
254
+ const counts = [
255
+ ["installed", grouped.officialUpstream.length],
256
+ ["resolved", grouped.githubResolved.length],
257
+ ["docs-discovered", grouped.wellKnownResolved.length],
258
+ ["metadata-routed", grouped.metadataRouted.length],
259
+ ["skipped", skipped.length]
260
+ ];
261
+ const coverage = counts.filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`).join(", ");
262
+ return `## Module guides
263
+ Module guidance exists for installed packages. Keep it scoped to module-owned APIs, config, runtime behavior, plugins, composables, components, hooks, and owned files.
264
+
265
+ - Inventory: [Module guide index](./references/modules/index.md)
266
+ - Coverage: ${coverage || "none"}`;
267
+ }
268
+ function createModuleIndex(entries, skipped = []) {
269
+ if (!hasModuleGuidance(entries, skipped)) {
270
+ return "";
271
+ }
272
+ const grouped = groupModuleEntries(entries);
273
+ const moduleSections = [
274
+ renderModuleGroup("Official upstream skills", grouped.officialUpstream, "./"),
275
+ renderModuleGroup("Resolved module skills", grouped.githubResolved, "./"),
276
+ renderModuleGroup("Docs-discovered skills", grouped.wellKnownResolved, "./"),
277
+ renderModuleGroup("Metadata-routed skills", grouped.metadataRouted, "./"),
278
+ renderSkippedEntries(skipped)
279
+ ].filter(Boolean);
280
+ return `# Module Guide Index
281
+
282
+ Use a module entry only when an installed module owns the surface. Module guidance is delta-only and should not replace broad Nuxt rules outside that module.
283
+
284
+ ${moduleSections.join("\n")}`;
285
+ }
244
286
  function findPack(metadata, id) {
245
287
  return metadata.packs.find((pack) => pack.id === id) || metadata.packs[0];
246
288
  }
@@ -325,24 +367,12 @@ function createRoutingExamples(metadata, includeModuleAuthoring = false) {
325
367
  ${lines.join("\n")}`;
326
368
  }
327
369
  function createSkillMapSections(metadata, entries, skipped = [], includeModuleAuthoring = false) {
328
- const includeModuleSections = hasModuleGuidance(entries, skipped);
329
- const grouped = groupModuleEntries(entries);
330
- const moduleSections = [
331
- renderModuleGroup("Official upstream skills", grouped.officialUpstream, "./references/modules/"),
332
- renderModuleGroup("Resolved module skills", grouped.githubResolved, "./references/modules/"),
333
- renderModuleGroup("Metadata-routed skills", grouped.metadataRouted, "./references/modules/"),
334
- renderSkippedEntries(skipped)
335
- ].filter(Boolean);
336
- const moduleGuidesSection = includeModuleSections ? `## Module guides
337
- Use a module entry only when an installed module owns the surface. Module guidance is delta-only and should not replace broad Nuxt rules outside that module.
338
-
339
- ${moduleSections.join("\n")}` : "";
340
370
  return [
341
371
  "## Routing",
342
372
  "Load one matching guide first. Open more guides only when the first guide points you there or the task clearly crosses boundaries.",
343
373
  createRoutingTable(metadata, includeModuleAuthoring),
344
374
  createRoutingExamples(metadata, includeModuleAuthoring),
345
- moduleGuidesSection
375
+ renderModuleSummary(entries, skipped)
346
376
  ].filter(Boolean).join("\n\n");
347
377
  }
348
378
  function createSkillEntrypoint(skillName, metadata, monorepoScopePath, includeModuleAuthoring = false, entries = [], skipped = []) {
@@ -421,6 +451,23 @@ function createCompactMetadataRouterContent(entry) {
421
451
  })}
422
452
  `;
423
453
  }
454
+ function createMetadataRouterContent(entry) {
455
+ const links = createCompactMetadataRouterContent(entry).trim();
456
+ const sourceSection = links || "Docs: unavailable\n\nSource code: unavailable";
457
+ return `# ${entry.packageName} Module Router
458
+
459
+ Use this module router only when the task directly involves \`${entry.packageName}\` APIs, config, runtime behavior, plugins, composables, components, hooks, or owned files.
460
+
461
+ ## Source of truth
462
+ ${sourceSection}
463
+
464
+ ## Context rules
465
+ 1. Start with the relevant route in the parent Nuxt skill before using this module router.
466
+ 2. Use the docs link for exact APIs, options, and examples instead of guessing from generic Nuxt or Vue patterns.
467
+ 3. Use the source link when behavior is version-sensitive or the docs are ambiguous.
468
+ 4. Keep this module guidance scoped to \`${entry.packageName}\`; do not replace broader Nuxt rules outside module-owned surfaces.
469
+ `;
470
+ }
424
471
 
425
472
  async function loadSkillFilesFromDir(dir, skip) {
426
473
  const skipSet = skip ? new Set(skip) : void 0;
@@ -494,24 +541,15 @@ function resolveSkillsDir(target, config) {
494
541
  }
495
542
  return void 0;
496
543
  }
497
- function normalizeTarget(target) {
498
- return target.trim();
499
- }
500
- function getRawTargetConfig(target) {
501
- return getAgentConfig(target);
502
- }
503
544
  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));
545
+ return getAgentIds().filter((target) => Boolean(resolveSkillsDir(target, getAgentConfig(target)))).sort((a, b) => a.localeCompare(b));
508
546
  }
509
547
  function resolveAgentTargetConfig(target) {
510
- const normalized = normalizeTarget(target);
548
+ const normalized = target.trim();
511
549
  if (!normalized) {
512
550
  return void 0;
513
551
  }
514
- const config = getRawTargetConfig(normalized);
552
+ const config = getAgentConfig(normalized);
515
553
  const skillsDir = resolveSkillsDir(normalized, config);
516
554
  if (!config || !skillsDir) {
517
555
  return void 0;
@@ -523,11 +561,11 @@ function resolveAgentTargetConfig(target) {
523
561
  };
524
562
  }
525
563
  function validateTargets(targets) {
526
- const uniqueTargets = Array.from(new Set(targets.map(normalizeTarget).filter(Boolean)));
564
+ const uniqueTargets = Array.from(new Set(targets.map((target) => target.trim()).filter(Boolean)));
527
565
  const valid = [];
528
566
  const invalid = [];
529
567
  for (const target of uniqueTargets) {
530
- const config = getRawTargetConfig(target);
568
+ const config = getAgentConfig(target);
531
569
  if (!config) {
532
570
  invalid.push({
533
571
  target,
@@ -562,6 +600,24 @@ const MANAGED_HINT_START = "<!-- nuxt-skill-hub:start -->";
562
600
  const MANAGED_HINT_END = "<!-- nuxt-skill-hub:end -->";
563
601
  const SKILL_NAME_MAX_LENGTH = 64;
564
602
  const SKILL_NAME_PATTERN = /^[a-z0-9-]+$/;
603
+ async function mapWithConcurrency(items, concurrency, mapper) {
604
+ if (!items.length) {
605
+ return [];
606
+ }
607
+ const results = [];
608
+ let nextIndex = 0;
609
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
610
+ while (true) {
611
+ const index = nextIndex++;
612
+ if (index >= items.length) {
613
+ return;
614
+ }
615
+ results[index] = await mapper(items[index], index);
616
+ }
617
+ });
618
+ await Promise.all(workers);
619
+ return results;
620
+ }
565
621
  async function pathExists(path) {
566
622
  try {
567
623
  await promises.access(path);
@@ -725,6 +781,22 @@ function readRepositoryUrl(repository) {
725
781
  if (repository && typeof repository.url === "string") return repository.url;
726
782
  return void 0;
727
783
  }
784
+ function readUrlValues(value) {
785
+ if (Array.isArray(value)) {
786
+ return value.flatMap(readUrlValues);
787
+ }
788
+ if (typeof value === "string" && value.trim()) {
789
+ return [value.trim()];
790
+ }
791
+ if (value && typeof value === "object" && "url" in value) {
792
+ const url = value.url;
793
+ return typeof url === "string" && url.trim() ? [url.trim()] : [];
794
+ }
795
+ return [];
796
+ }
797
+ function dedupe$1(values) {
798
+ return [...new Set(values.filter((value) => Boolean(value)))];
799
+ }
728
800
  async function discoverInstalledPackageFromSpecifier(specifier, rootDir) {
729
801
  if (!isPackageSpecifier(specifier)) {
730
802
  return null;
@@ -746,8 +818,29 @@ async function discoverInstalledPackageFromDirectory(directory) {
746
818
  }
747
819
  return readInstalledPackageMetadata(packageJsonPath);
748
820
  }
821
+ async function collectInstalledModulePackages(modules, rootDir) {
822
+ const specifiers = Array.from(new Set(
823
+ (modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry))
824
+ ));
825
+ const resolved = await Promise.all(
826
+ specifiers.map((specifier) => discoverInstalledPackageFromSpecifier(specifier, rootDir))
827
+ );
828
+ const seenPackageRoots = /* @__PURE__ */ new Set();
829
+ const packages = [];
830
+ for (const pkg of resolved) {
831
+ if (!pkg || seenPackageRoots.has(pkg.packageRoot)) continue;
832
+ seenPackageRoots.add(pkg.packageRoot);
833
+ packages.push(pkg);
834
+ }
835
+ return packages;
836
+ }
749
837
  async function readInstalledPackageMetadata(packageJsonPath, fallbackSpecifier) {
750
838
  const pkg = await readPackageJSON(packageJsonPath);
839
+ const docsUrls = dedupe$1([
840
+ ...readUrlValues(pkg.docs),
841
+ ...readUrlValues(pkg.documentation),
842
+ ...readUrlValues(pkg.homepage)
843
+ ]);
751
844
  return {
752
845
  packageName: pkg.name || (fallbackSpecifier ? packageNameFromSpecifier(fallbackSpecifier) : basename(dirname(packageJsonPath))),
753
846
  version: pkg.version,
@@ -755,6 +848,7 @@ async function readInstalledPackageMetadata(packageJsonPath, fallbackSpecifier)
755
848
  packageRoot: dirname(packageJsonPath),
756
849
  repository: readRepositoryUrl(pkg.repository),
757
850
  homepage: pkg.homepage,
851
+ docsUrls,
758
852
  packageData: pkg
759
853
  };
760
854
  }
@@ -863,15 +957,15 @@ async function validateContribution(contribution) {
863
957
  };
864
958
  }
865
959
  async function validateResolvedContributions(contributions) {
960
+ const validations = await Promise.all(contributions.map(validateContribution));
866
961
  const valid = [];
867
962
  const issues = [];
868
- for (const contribution of contributions) {
869
- const validation = await validateContribution(contribution);
870
- if (!validation.issues.length) {
871
- valid.push(validation.contribution);
963
+ for (const validation of validations) {
964
+ if (validation.issues.length) {
965
+ issues.push(...validation.issues);
872
966
  continue;
873
967
  }
874
- issues.push(...validation.issues);
968
+ valid.push(validation.contribution);
875
969
  }
876
970
  return {
877
971
  contributions: sortAndDedupeContributions(valid),
@@ -1040,7 +1134,7 @@ function formatConflictWarning(conflict) {
1040
1134
  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
1135
  }
1042
1136
 
1043
- const GITHUB_OVERRIDES = [
1137
+ const PACKAGE_OVERRIDES = [
1044
1138
  {
1045
1139
  packageName: "@nuxt/ui",
1046
1140
  repo: "nuxt/ui",
@@ -1054,10 +1148,15 @@ const GITHUB_OVERRIDES = [
1054
1148
  ref: "main",
1055
1149
  path: "skills/vueuse-functions",
1056
1150
  skillName: "vueuse-functions"
1151
+ },
1152
+ {
1153
+ packageName: "docus",
1154
+ repo: "nuxt-content/docus",
1155
+ docsUrls: ["https://docus.dev/"]
1057
1156
  }
1058
1157
  ];
1059
- function findGitHubOverride(packageName) {
1060
- return GITHUB_OVERRIDES.find((entry) => entry.packageName === packageName);
1158
+ function findPackageOverride(packageName) {
1159
+ return PACKAGE_OVERRIDES.find((entry) => entry.packageName === packageName);
1061
1160
  }
1062
1161
 
1063
1162
  const GITHUB_API_BASE = "https://api.github.com";
@@ -1065,6 +1164,8 @@ const UNGH_API_BASE = "https://ungh.cc";
1065
1164
  const githubDirectoryCache = /* @__PURE__ */ new Map();
1066
1165
  const githubDefaultBranchCache = /* @__PURE__ */ new Map();
1067
1166
  const githubFileTextCache = /* @__PURE__ */ new Map();
1167
+ const urlTextCache = /* @__PURE__ */ new Map();
1168
+ const urlBytesCache = /* @__PURE__ */ new Map();
1068
1169
  function githubFetchOptions(timeoutMs) {
1069
1170
  return {
1070
1171
  timeout: timeoutMs,
@@ -1074,6 +1175,9 @@ function githubFetchOptions(timeoutMs) {
1074
1175
  }
1075
1176
  };
1076
1177
  }
1178
+ function fetchStatus(error) {
1179
+ return typeof error === "object" && error && "status" in error ? Number(error.status) : void 0;
1180
+ }
1077
1181
  function encodeGitHubPath(path) {
1078
1182
  return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
1079
1183
  }
@@ -1180,16 +1284,591 @@ async function fetchGitHubFileText(repo, ref, filePath, timeoutMs) {
1180
1284
  );
1181
1285
  return { ok: true, data };
1182
1286
  } catch (error) {
1183
- const status = typeof error === "object" && error && "status" in error ? Number(error.status) : void 0;
1184
1287
  return {
1185
1288
  ok: false,
1186
- status,
1289
+ status: fetchStatus(error),
1290
+ error: error.message
1291
+ };
1292
+ }
1293
+ });
1294
+ }
1295
+ async function fetchUrlText(url, timeoutMs) {
1296
+ return await memoize(urlTextCache, url, async () => {
1297
+ try {
1298
+ const data = await ofetch(url, {
1299
+ ...githubFetchOptions(timeoutMs),
1300
+ responseType: "text"
1301
+ });
1302
+ return { ok: true, data };
1303
+ } catch (error) {
1304
+ return {
1305
+ ok: false,
1306
+ status: fetchStatus(error),
1307
+ error: error.message
1308
+ };
1309
+ }
1310
+ });
1311
+ }
1312
+ async function fetchUrlJson(url, timeoutMs) {
1313
+ const text = await fetchUrlText(url, timeoutMs);
1314
+ if (!text.ok) {
1315
+ return {
1316
+ ok: false,
1317
+ status: text.status,
1318
+ error: text.error
1319
+ };
1320
+ }
1321
+ try {
1322
+ return {
1323
+ ok: true,
1324
+ data: JSON.parse(text.data || "")
1325
+ };
1326
+ } catch (error) {
1327
+ return {
1328
+ ok: false,
1329
+ error: error.message
1330
+ };
1331
+ }
1332
+ }
1333
+ async function fetchUrlBytes(url, timeoutMs) {
1334
+ return await memoize(urlBytesCache, url, async () => {
1335
+ try {
1336
+ const data = await ofetch(url, {
1337
+ ...githubFetchOptions(timeoutMs),
1338
+ responseType: "arrayBuffer"
1339
+ });
1340
+ return {
1341
+ ok: true,
1342
+ data: new Uint8Array(data)
1343
+ };
1344
+ } catch (error) {
1345
+ return {
1346
+ ok: false,
1347
+ status: fetchStatus(error),
1187
1348
  error: error.message
1188
1349
  };
1189
1350
  }
1190
1351
  });
1191
1352
  }
1192
1353
 
1354
+ const DEFAULT_SKILL_HUB_GENERATION_MODE = "prepare";
1355
+ const DEFAULT_REMOTE_TIMEOUT_MS = 1500;
1356
+ const DEFAULT_REMOTE_CACHE_TTL_MS = 1e3 * 60 * 60 * 24;
1357
+ const DEFAULT_REMOTE_CONCURRENCY = 8;
1358
+ const DEFAULT_WELL_KNOWN_MAX_FILES = 40;
1359
+ const DEFAULT_WELL_KNOWN_MAX_BYTES = 512 * 1024;
1360
+ const DEFAULT_WELL_KNOWN_MAX_FILE_BYTES = 128 * 1024;
1361
+ function normalizeGenerationMode(value) {
1362
+ return value === "manual" ? "manual" : DEFAULT_SKILL_HUB_GENERATION_MODE;
1363
+ }
1364
+ function positiveNumber(value, fallback) {
1365
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1366
+ }
1367
+ function nonNegativeNumber(value, fallback) {
1368
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
1369
+ }
1370
+ function positiveInteger(value, fallback) {
1371
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
1372
+ }
1373
+ function normalizeRemoteOptions(value) {
1374
+ const options = typeof value === "object" && value ? value : {};
1375
+ const enabled = typeof value === "boolean" ? value : options.enabled !== false;
1376
+ return {
1377
+ enabled,
1378
+ timeoutMs: positiveNumber(options.timeoutMs, DEFAULT_REMOTE_TIMEOUT_MS),
1379
+ concurrency: positiveInteger(options.concurrency, DEFAULT_REMOTE_CONCURRENCY),
1380
+ refresh: options.refresh === true,
1381
+ cacheTtlMs: nonNegativeNumber(options.cacheTtlMs, DEFAULT_REMOTE_CACHE_TTL_MS),
1382
+ githubHeuristics: options.githubHeuristics === true,
1383
+ timings: options.timings === true,
1384
+ maxFiles: positiveNumber(options.maxFiles, DEFAULT_WELL_KNOWN_MAX_FILES),
1385
+ maxBytes: positiveNumber(options.maxBytes, DEFAULT_WELL_KNOWN_MAX_BYTES),
1386
+ maxFileBytes: positiveNumber(options.maxFileBytes, DEFAULT_WELL_KNOWN_MAX_FILE_BYTES)
1387
+ };
1388
+ }
1389
+
1390
+ const WELL_KNOWN_DISCOVERY_V2_SCHEMA = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
1391
+ const WELL_KNOWN_DISCOVERY_V2_INDEX_PATH = "/.well-known/agent-skills/index.json";
1392
+ const WELL_KNOWN_SKILLS_INDEX_PATH = "/.well-known/skills/index.json";
1393
+ const WELL_KNOWN_ENTRY_CONCURRENCY = 4;
1394
+ const WELL_KNOWN_FILE_CONCURRENCY = 8;
1395
+ function normalizeLimits(limits = {}) {
1396
+ return {
1397
+ maxFiles: typeof limits.maxFiles === "number" && Number.isFinite(limits.maxFiles) && limits.maxFiles > 0 ? limits.maxFiles : DEFAULT_WELL_KNOWN_MAX_FILES,
1398
+ maxBytes: typeof limits.maxBytes === "number" && Number.isFinite(limits.maxBytes) && limits.maxBytes > 0 ? limits.maxBytes : DEFAULT_WELL_KNOWN_MAX_BYTES,
1399
+ maxFileBytes: typeof limits.maxFileBytes === "number" && Number.isFinite(limits.maxFileBytes) && limits.maxFileBytes > 0 ? limits.maxFileBytes : DEFAULT_WELL_KNOWN_MAX_FILE_BYTES
1400
+ };
1401
+ }
1402
+ function makeSkip$1(packageName, skillName, reason) {
1403
+ return {
1404
+ packageName,
1405
+ skillName,
1406
+ reason,
1407
+ sourceKind: "wellKnown"
1408
+ };
1409
+ }
1410
+ function normalizeHttpUrl$1(url) {
1411
+ const trimmed = url?.trim();
1412
+ if (!trimmed) {
1413
+ return void 0;
1414
+ }
1415
+ try {
1416
+ const parsed = new URL(trimmed);
1417
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString() : void 0;
1418
+ } catch {
1419
+ return void 0;
1420
+ }
1421
+ }
1422
+ function normalizeHostname(hostname) {
1423
+ return hostname.trim().toLowerCase().replace(/^\[|\]$/g, "");
1424
+ }
1425
+ function parseIpv4(hostname) {
1426
+ const parts = hostname.split(".");
1427
+ if (parts.length !== 4) {
1428
+ return null;
1429
+ }
1430
+ const octets = parts.map((part) => {
1431
+ if (!/^\d+$/.test(part)) {
1432
+ return Number.NaN;
1433
+ }
1434
+ return Number(part);
1435
+ });
1436
+ if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
1437
+ return null;
1438
+ }
1439
+ return octets;
1440
+ }
1441
+ function isPrivateIpv4(hostname) {
1442
+ const octets = parseIpv4(normalizeHostname(hostname));
1443
+ if (!octets) {
1444
+ return false;
1445
+ }
1446
+ const [a, b] = octets;
1447
+ 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;
1448
+ }
1449
+ function isPrivateIpv6(hostname) {
1450
+ const normalized = normalizeHostname(hostname);
1451
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
1452
+ }
1453
+ function isUnsafeHostname(hostname) {
1454
+ const normalized = normalizeHostname(hostname);
1455
+ if (normalized === "localhost" || normalized.endsWith(".localhost")) {
1456
+ return true;
1457
+ }
1458
+ const ipVersion = isIP(normalized);
1459
+ if (ipVersion === 4) {
1460
+ return isPrivateIpv4(normalized);
1461
+ }
1462
+ if (ipVersion === 6) {
1463
+ return isPrivateIpv6(normalized);
1464
+ }
1465
+ return false;
1466
+ }
1467
+ function isPublicDiscoveryBase(url) {
1468
+ try {
1469
+ const parsed = new URL(url);
1470
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.hostname === "github.com" || parseGitHubRepo(url) || isUnsafeHostname(parsed.hostname)) {
1471
+ return false;
1472
+ }
1473
+ return true;
1474
+ } catch {
1475
+ return false;
1476
+ }
1477
+ }
1478
+ function isSameOriginUrl(url, baseUrl) {
1479
+ return new URL(url).origin === new URL(baseUrl).origin;
1480
+ }
1481
+ function resolveSameOriginUrl(rawUrl, baseUrl) {
1482
+ const trimmed = rawUrl.trim();
1483
+ if (!trimmed) {
1484
+ return null;
1485
+ }
1486
+ try {
1487
+ const resolved = new URL(trimmed, baseUrl);
1488
+ if (!isSameOriginUrl(resolved.toString(), baseUrl) || isUnsafeHostname(resolved.hostname)) {
1489
+ return null;
1490
+ }
1491
+ return resolved.toString();
1492
+ } catch {
1493
+ return null;
1494
+ }
1495
+ }
1496
+ function docsBaseCandidates(packageInfo) {
1497
+ const override = findPackageOverride(packageInfo.packageName);
1498
+ const candidates = [
1499
+ ...override?.docsUrls || [],
1500
+ ...packageInfo.docsUrls || [],
1501
+ packageInfo.homepage || ""
1502
+ ];
1503
+ return [...new Set(candidates.map(normalizeHttpUrl$1).filter((url) => Boolean(url)).filter(isPublicDiscoveryBase))];
1504
+ }
1505
+ function normalizeDigest(value) {
1506
+ if (typeof value !== "string") {
1507
+ return null;
1508
+ }
1509
+ const trimmed = value.trim();
1510
+ return /^sha256:[a-f0-9]{64}$/.test(trimmed) ? trimmed : null;
1511
+ }
1512
+ function sha256(bytes) {
1513
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
1514
+ }
1515
+ function normalizeSkillFilePath(value) {
1516
+ if (typeof value !== "string") {
1517
+ return null;
1518
+ }
1519
+ const trimmed = value.trim().replaceAll("\\", "/");
1520
+ if (!trimmed || trimmed.startsWith("/") || trimmed.includes("\0")) {
1521
+ return null;
1522
+ }
1523
+ const segments = trimmed.split("/");
1524
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
1525
+ return null;
1526
+ }
1527
+ return trimmed;
1528
+ }
1529
+ function isSupportedSkillContextFile(path) {
1530
+ return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".json");
1531
+ }
1532
+ async function writeSkillFile(root, relativePath, bytes) {
1533
+ const destination = join(root, relativePath);
1534
+ await ensureDir(dirname(destination));
1535
+ await promises.writeFile(destination, bytes);
1536
+ }
1537
+ function contributionFor(input) {
1538
+ const sourceRepo = parseGitHubRepo(input.packageInfo.repository);
1539
+ return normalizeContribution({
1540
+ packageName: input.packageInfo.packageName,
1541
+ version: input.packageInfo.version,
1542
+ sourceDir: input.targetDir,
1543
+ skillName: input.skillName,
1544
+ description: input.description,
1545
+ sourceKind: "wellKnown",
1546
+ sourceRepo: sourceRepo || void 0,
1547
+ sourcePath: input.indexUrl,
1548
+ repoUrl: sourceRepo ? `https://github.com/${sourceRepo}` : void 0,
1549
+ docsUrl: input.docsUrl,
1550
+ official: true,
1551
+ resolver: input.resolver,
1552
+ forceIncludeScripts: true
1553
+ }, input.targetDir, join(input.targetDir, ".."));
1554
+ }
1555
+ async function materializeSkillsIndexEntry(input) {
1556
+ const skillName = typeof input.entry.name === "string" ? input.entry.name.trim() : "";
1557
+ if (!isValidSkillName(skillName)) {
1558
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName || "entry", "well-known skill name is invalid") };
1559
+ }
1560
+ const description = typeof input.entry.description === "string" ? input.entry.description.trim() : "";
1561
+ if (!description) {
1562
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known skill is missing a description") };
1563
+ }
1564
+ if (!Array.isArray(input.entry.files)) {
1565
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known skill is missing a files array") };
1566
+ }
1567
+ const files = [...new Set(input.entry.files.map(normalizeSkillFilePath).filter((file) => Boolean(file)))];
1568
+ if (!files.includes("SKILL.md")) {
1569
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known skill must list SKILL.md") };
1570
+ }
1571
+ if (files.length !== input.entry.files.length) {
1572
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known skill includes an unsafe file path") };
1573
+ }
1574
+ if (files.some((file) => !isSupportedSkillContextFile(file))) {
1575
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known skill includes a non-context file; only .md and .json files are supported") };
1576
+ }
1577
+ if (files.length > input.limits.maxFiles) {
1578
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, `well-known skill lists ${files.length} files, exceeding the limit of ${input.limits.maxFiles}`) };
1579
+ }
1580
+ const origin = new URL(input.docsUrl).origin;
1581
+ const targetDir = join(
1582
+ input.cacheRoot,
1583
+ "well-known",
1584
+ "skills",
1585
+ sanitizeSegment(origin),
1586
+ sanitizeSegment(input.packageInfo.packageName),
1587
+ sanitizeSegment(input.packageInfo.version || "unknown"),
1588
+ sanitizeSegment(skillName)
1589
+ );
1590
+ const fileUrls = files.map((file) => {
1591
+ const fileUrl = resolveSameOriginUrl(`${skillName}/${file}`, new URL("./", input.indexUrl).toString());
1592
+ if (!fileUrl) {
1593
+ return null;
1594
+ }
1595
+ return { file, fileUrl };
1596
+ });
1597
+ if (fileUrls.includes(null)) {
1598
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known skill file URL must stay on the discovery origin") };
1599
+ }
1600
+ const fetchedFiles = [];
1601
+ let totalBytes = 0;
1602
+ const responses = await mapWithConcurrency(
1603
+ fileUrls,
1604
+ WELL_KNOWN_FILE_CONCURRENCY,
1605
+ async ({ file, fileUrl }) => ({
1606
+ file,
1607
+ response: await fetchUrlBytes(fileUrl, input.timeoutMs)
1608
+ })
1609
+ );
1610
+ for (const { file, response } of responses) {
1611
+ if (!response.ok || !response.data) {
1612
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, `failed to fetch well-known skill file ${file}`) };
1613
+ }
1614
+ if (response.data.byteLength > input.limits.maxFileBytes) {
1615
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, `well-known skill file ${file} exceeds the per-file limit of ${input.limits.maxFileBytes} bytes`) };
1616
+ }
1617
+ totalBytes += response.data.byteLength;
1618
+ if (totalBytes > input.limits.maxBytes) {
1619
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, `well-known skill files exceed the total limit of ${input.limits.maxBytes} bytes`) };
1620
+ }
1621
+ fetchedFiles.push({ path: file, bytes: response.data });
1622
+ }
1623
+ await emptyDir(targetDir);
1624
+ for (const file of fetchedFiles) {
1625
+ await writeSkillFile(targetDir, file.path, file.bytes);
1626
+ }
1627
+ return {
1628
+ contribution: contributionFor({
1629
+ packageInfo: input.packageInfo,
1630
+ targetDir,
1631
+ skillName,
1632
+ description,
1633
+ docsUrl: input.docsUrl,
1634
+ resolver: "wellKnownSkills",
1635
+ indexUrl: input.indexUrl
1636
+ })
1637
+ };
1638
+ }
1639
+ async function materializeV2Skill(input) {
1640
+ const skillName = typeof input.entry.name === "string" ? input.entry.name.trim() : "";
1641
+ if (!isValidSkillName(skillName)) {
1642
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName || "entry", "well-known v2 skill name is invalid") };
1643
+ }
1644
+ const description = typeof input.entry.description === "string" ? input.entry.description.trim() : "";
1645
+ if (!description) {
1646
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill is missing a description") };
1647
+ }
1648
+ const type = typeof input.entry.type === "string" ? input.entry.type.trim() : "";
1649
+ if (type === "archive") {
1650
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "archive artifacts are not supported yet") };
1651
+ }
1652
+ if (type !== "skill-md") {
1653
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, `unsupported well-known v2 skill type "${type || "unknown"}"`) };
1654
+ }
1655
+ if (typeof input.entry.url !== "string" || !input.entry.url.trim()) {
1656
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill is missing a URL") };
1657
+ }
1658
+ const digest = normalizeDigest(input.entry.digest);
1659
+ if (!digest) {
1660
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill is missing a valid sha256 digest") };
1661
+ }
1662
+ const skillUrl = resolveSameOriginUrl(input.entry.url, input.indexUrl);
1663
+ if (!skillUrl) {
1664
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill URL must stay on the discovery origin") };
1665
+ }
1666
+ const response = await fetchUrlBytes(skillUrl, input.timeoutMs);
1667
+ if (!response.ok || !response.data) {
1668
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "failed to fetch well-known v2 skill artifact") };
1669
+ }
1670
+ if (sha256(response.data) !== digest) {
1671
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill digest mismatch") };
1672
+ }
1673
+ if (response.data.byteLength > input.limits.maxFileBytes || response.data.byteLength > input.limits.maxBytes) {
1674
+ return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, `well-known v2 skill exceeds the file size limit of ${Math.min(input.limits.maxFileBytes, input.limits.maxBytes)} bytes`) };
1675
+ }
1676
+ const origin = new URL(input.docsUrl).origin;
1677
+ const targetDir = join(
1678
+ input.cacheRoot,
1679
+ "well-known",
1680
+ "rfc",
1681
+ sanitizeSegment(origin),
1682
+ sanitizeSegment(input.packageInfo.packageName),
1683
+ sanitizeSegment(input.packageInfo.version || "unknown"),
1684
+ sanitizeSegment(skillName)
1685
+ );
1686
+ await emptyDir(targetDir);
1687
+ await writeSkillFile(targetDir, "SKILL.md", response.data);
1688
+ return {
1689
+ contribution: contributionFor({
1690
+ packageInfo: input.packageInfo,
1691
+ targetDir,
1692
+ skillName,
1693
+ description,
1694
+ docsUrl: input.docsUrl,
1695
+ resolver: "wellKnownV2",
1696
+ indexUrl: input.indexUrl
1697
+ })
1698
+ };
1699
+ }
1700
+ async function resolveSkillsIndex(input) {
1701
+ const skipped = [];
1702
+ const contributions = [];
1703
+ if (!Array.isArray(input.index.skills)) {
1704
+ return {
1705
+ contributions: [],
1706
+ issues: [],
1707
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known skills index is missing a skills array")]
1708
+ };
1709
+ }
1710
+ const materializedEntries = await mapWithConcurrency(
1711
+ input.index.skills,
1712
+ WELL_KNOWN_ENTRY_CONCURRENCY,
1713
+ async (rawEntry) => {
1714
+ if (!rawEntry || typeof rawEntry !== "object") {
1715
+ return { skipped: makeSkip$1(input.packageInfo.packageName, "entry", "well-known skill entry must be an object") };
1716
+ }
1717
+ return await materializeSkillsIndexEntry({
1718
+ packageInfo: input.packageInfo,
1719
+ entry: rawEntry,
1720
+ indexUrl: input.indexUrl,
1721
+ docsUrl: input.docsUrl,
1722
+ cacheRoot: input.cacheRoot,
1723
+ timeoutMs: input.timeoutMs,
1724
+ limits: input.limits
1725
+ });
1726
+ }
1727
+ );
1728
+ for (const materialized of materializedEntries) {
1729
+ if (materialized?.contribution) {
1730
+ contributions.push(materialized.contribution);
1731
+ }
1732
+ if (materialized?.skipped) {
1733
+ skipped.push(materialized.skipped);
1734
+ }
1735
+ }
1736
+ return { contributions, issues: [], skipped };
1737
+ }
1738
+ async function resolveV2Index(input) {
1739
+ const skipped = [];
1740
+ const contributions = [];
1741
+ if (!Array.isArray(input.index.skills)) {
1742
+ return {
1743
+ contributions: [],
1744
+ issues: [],
1745
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known v2 index is missing a skills array")]
1746
+ };
1747
+ }
1748
+ const materializedEntries = await mapWithConcurrency(
1749
+ input.index.skills,
1750
+ WELL_KNOWN_ENTRY_CONCURRENCY,
1751
+ async (rawEntry) => {
1752
+ if (!rawEntry || typeof rawEntry !== "object") {
1753
+ return { skipped: makeSkip$1(input.packageInfo.packageName, "entry", "well-known v2 skill entry must be an object") };
1754
+ }
1755
+ return await materializeV2Skill({
1756
+ packageInfo: input.packageInfo,
1757
+ entry: rawEntry,
1758
+ indexUrl: input.indexUrl,
1759
+ docsUrl: input.docsUrl,
1760
+ cacheRoot: input.cacheRoot,
1761
+ timeoutMs: input.timeoutMs,
1762
+ limits: input.limits
1763
+ });
1764
+ }
1765
+ );
1766
+ for (const materialized of materializedEntries) {
1767
+ if (materialized?.contribution) {
1768
+ contributions.push(materialized.contribution);
1769
+ }
1770
+ if (materialized?.skipped) {
1771
+ skipped.push(materialized.skipped);
1772
+ }
1773
+ }
1774
+ return { contributions, issues: [], skipped };
1775
+ }
1776
+ async function resolveIndex(input) {
1777
+ const response = await fetchUrlJson(input.indexUrl, input.timeoutMs);
1778
+ if (!response.ok) {
1779
+ if (response.status === 404) {
1780
+ return null;
1781
+ }
1782
+ return {
1783
+ contributions: [],
1784
+ issues: [],
1785
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, `failed to fetch well-known index ${input.indexUrl}`)]
1786
+ };
1787
+ }
1788
+ const index = response.data;
1789
+ if (!index || typeof index !== "object") {
1790
+ return {
1791
+ contributions: [],
1792
+ issues: [],
1793
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known index must be an object")]
1794
+ };
1795
+ }
1796
+ if (index.$schema === WELL_KNOWN_DISCOVERY_V2_SCHEMA) {
1797
+ return await resolveV2Index({
1798
+ ...input,
1799
+ index
1800
+ });
1801
+ }
1802
+ if (new URL(input.indexUrl).pathname === WELL_KNOWN_SKILLS_INDEX_PATH && !index.$schema) {
1803
+ return await resolveSkillsIndex({
1804
+ ...input,
1805
+ index
1806
+ });
1807
+ }
1808
+ if (typeof index.$schema === "string" && index.$schema) {
1809
+ return {
1810
+ contributions: [],
1811
+ issues: [],
1812
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, `unsupported well-known schema "${index.$schema}"`)]
1813
+ };
1814
+ }
1815
+ return {
1816
+ contributions: [],
1817
+ issues: [],
1818
+ skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known index is missing the v2 schema")]
1819
+ };
1820
+ }
1821
+ async function resolveViaWellKnown(packageInfo, cacheRoot, timeoutMs, rawLimits) {
1822
+ const bases = docsBaseCandidates(packageInfo);
1823
+ const skipped = [];
1824
+ const issues = [];
1825
+ const limits = normalizeLimits(rawLimits);
1826
+ const attempts = bases.flatMap((docsUrl) => {
1827
+ return [
1828
+ new URL(WELL_KNOWN_DISCOVERY_V2_INDEX_PATH, docsUrl).toString(),
1829
+ new URL(WELL_KNOWN_SKILLS_INDEX_PATH, docsUrl).toString()
1830
+ ].map((indexUrl) => ({ docsUrl, indexUrl }));
1831
+ });
1832
+ const results = await Promise.all(
1833
+ attempts.map(async ({ docsUrl, indexUrl }) => ({
1834
+ result: await resolveIndex({
1835
+ packageInfo,
1836
+ indexUrl,
1837
+ docsUrl,
1838
+ cacheRoot,
1839
+ timeoutMs,
1840
+ limits
1841
+ })
1842
+ }))
1843
+ );
1844
+ for (const { result } of results) {
1845
+ if (!result) {
1846
+ continue;
1847
+ }
1848
+ issues.push(...result.issues);
1849
+ skipped.push(...result.skipped);
1850
+ if (result.contributions.length) {
1851
+ return {
1852
+ contributions: result.contributions,
1853
+ issues,
1854
+ skipped
1855
+ };
1856
+ }
1857
+ }
1858
+ if (!bases.length) {
1859
+ return {
1860
+ contributions: [],
1861
+ issues: [],
1862
+ skipped: []
1863
+ };
1864
+ }
1865
+ return {
1866
+ contributions: [],
1867
+ issues,
1868
+ skipped
1869
+ };
1870
+ }
1871
+
1193
1872
  function makeSkip(packageName, skillName, reason, sourceKind) {
1194
1873
  return {
1195
1874
  packageName,
@@ -1231,8 +1910,15 @@ function normalizeHttpUrl(url) {
1231
1910
  return void 0;
1232
1911
  }
1233
1912
  }
1913
+ function resolveDocsUrl(packageInfo) {
1914
+ const override = findPackageOverride(packageInfo.packageName);
1915
+ const overrideDocsUrl = override?.docsUrls?.map(normalizeHttpUrl).find(Boolean);
1916
+ const packageDocsUrl = packageInfo.docsUrls?.map(normalizeHttpUrl).find(Boolean);
1917
+ return overrideDocsUrl || packageDocsUrl || normalizeHttpUrl(packageInfo.homepage);
1918
+ }
1234
1919
  function resolveRepositoryUrl(packageInfo) {
1235
- const sourceRepo = parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
1920
+ const override = findPackageOverride(packageInfo.packageName);
1921
+ const sourceRepo = override?.repo || parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
1236
1922
  if (sourceRepo) {
1237
1923
  return {
1238
1924
  sourceRepo,
@@ -1304,18 +1990,9 @@ async function materializeCandidate(packageInfo, candidate, cacheRoot) {
1304
1990
  forceIncludeScripts: true
1305
1991
  }, targetDir, join(targetDir, ".."));
1306
1992
  }
1307
- async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1308
- const issues = [];
1309
- const skipped = [];
1310
- const override = findGitHubOverride(packageInfo.packageName);
1993
+ function createGitHubResolveContext(packageInfo) {
1994
+ const override = findPackageOverride(packageInfo.packageName);
1311
1995
  const repo = override?.repo || parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
1312
- if (!repo) {
1313
- return {
1314
- contributions: [],
1315
- issues,
1316
- skipped: [makeSkip(packageInfo.packageName, override?.skillName || packageInfo.packageName, "No GitHub repository metadata found", "github")]
1317
- };
1318
- }
1319
1996
  const refs = dedupe([
1320
1997
  override?.ref || "",
1321
1998
  packageInfo.version ? `v${packageInfo.version}` : "",
@@ -1335,35 +2012,89 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1335
2012
  ];
1336
2013
  })
1337
2014
  ]);
1338
- const tryResolveForRef = async (ref, allowTreeLookup) => {
1339
- const packageJson = await fetchGitHubFileText(repo, ref, "package.json", timeoutMs);
1340
- if (packageJson.ok && packageJson.data) {
1341
- try {
1342
- const parsed = JSON.parse(packageJson.data);
1343
- const remoteSkills = parseAgentSkillDeclarations(parsed, packageInfo.packageName, "github");
1344
- issues.push(...remoteSkills.issues);
1345
- for (const skill of remoteSkills.skills) {
1346
- const resolved = await materializeCandidate(packageInfo, {
1347
- skillName: skill.name,
1348
- sourcePath: skill.path,
1349
- sourceKind: "github",
1350
- sourceRepo: repo,
1351
- sourceRef: ref,
1352
- official: true,
1353
- resolver: "agentsField"
1354
- }, cacheRoot);
1355
- if (resolved) {
1356
- return [resolved];
1357
- }
1358
- }
1359
- } catch {
1360
- issues.push(createValidationIssue(packageInfo.packageName, packageInfo.packageName, "Failed to parse remote package.json", "github"));
2015
+ return {
2016
+ issues: [],
2017
+ skipped: [],
2018
+ override,
2019
+ repo: repo || void 0,
2020
+ refs,
2021
+ pathCandidates
2022
+ };
2023
+ }
2024
+ async function resolveAgentSkillsForRef(packageInfo, context, ref, cacheRoot, timeoutMs) {
2025
+ if (!context.repo) {
2026
+ return null;
2027
+ }
2028
+ const packageJson = await fetchGitHubFileText(context.repo, ref, "package.json", timeoutMs);
2029
+ if (!packageJson.ok || !packageJson.data) {
2030
+ return null;
2031
+ }
2032
+ try {
2033
+ const parsed = JSON.parse(packageJson.data);
2034
+ const remoteSkills = parseAgentSkillDeclarations(parsed, packageInfo.packageName, "github");
2035
+ context.issues.push(...remoteSkills.issues);
2036
+ for (const skill of remoteSkills.skills) {
2037
+ const resolved = await materializeCandidate(packageInfo, {
2038
+ skillName: skill.name,
2039
+ sourcePath: skill.path,
2040
+ sourceKind: "github",
2041
+ sourceRepo: context.repo,
2042
+ sourceRef: ref,
2043
+ official: true,
2044
+ resolver: "agentsField"
2045
+ }, cacheRoot);
2046
+ if (resolved) {
2047
+ return [resolved];
1361
2048
  }
1362
2049
  }
1363
- if (!allowTreeLookup) {
1364
- return null;
2050
+ } catch {
2051
+ context.issues.push(createValidationIssue(packageInfo.packageName, packageInfo.packageName, "Failed to parse remote package.json", "github"));
2052
+ }
2053
+ return null;
2054
+ }
2055
+ async function resolveViaGitHubAgents(packageInfo, cacheRoot, timeoutMs) {
2056
+ const context = createGitHubResolveContext(packageInfo);
2057
+ if (!context.repo) {
2058
+ return {
2059
+ contributions: [],
2060
+ issues: context.issues,
2061
+ skipped: []
2062
+ };
2063
+ }
2064
+ for (const ref of context.refs) {
2065
+ const resolved = await resolveAgentSkillsForRef(packageInfo, context, ref, cacheRoot, timeoutMs);
2066
+ if (resolved?.length) {
2067
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
2068
+ }
2069
+ }
2070
+ const fallbackRefs = dedupe([
2071
+ await fetchGitHubDefaultBranch(context.repo, timeoutMs) || "",
2072
+ "main",
2073
+ "master"
2074
+ ]);
2075
+ for (const ref of fallbackRefs) {
2076
+ const resolved = await resolveAgentSkillsForRef(packageInfo, context, ref, cacheRoot, timeoutMs);
2077
+ if (resolved?.length) {
2078
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
1365
2079
  }
1366
- const skillsDirs = await listGitHubDirectory(repo, ref, "skills", timeoutMs);
2080
+ }
2081
+ return {
2082
+ contributions: [],
2083
+ issues: context.issues,
2084
+ skipped: context.skipped
2085
+ };
2086
+ }
2087
+ async function resolveViaGitHubHeuristics(packageInfo, cacheRoot, timeoutMs) {
2088
+ const context = createGitHubResolveContext(packageInfo);
2089
+ if (!context.repo) {
2090
+ return {
2091
+ contributions: [],
2092
+ issues: context.issues,
2093
+ skipped: [makeSkip(packageInfo.packageName, context.override?.skillName || packageInfo.packageName, "No GitHub repository metadata found", "github")]
2094
+ };
2095
+ }
2096
+ const tryResolveForRef = async (ref, allowTreeLookup) => {
2097
+ const skillsDirs = await listGitHubDirectory(context.repo, ref, "skills", timeoutMs) ;
1367
2098
  if (skillsDirs.length) {
1368
2099
  const contributions = [];
1369
2100
  for (const skillName of skillsDirs) {
@@ -1371,7 +2102,7 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1371
2102
  skillName,
1372
2103
  sourcePath: `skills/${skillName}`,
1373
2104
  sourceKind: "github",
1374
- sourceRepo: repo,
2105
+ sourceRepo: context.repo,
1375
2106
  sourceRef: ref,
1376
2107
  official: true,
1377
2108
  resolver: "githubHeuristic"
@@ -1384,13 +2115,13 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1384
2115
  return contributions;
1385
2116
  }
1386
2117
  }
1387
- for (const path of pathCandidates) {
1388
- const candidateSkillName = override?.path && path === override.path ? override.skillName || path.split("/").pop() || packageInfo.packageName : path.split("/").pop() || packageInfo.packageName;
2118
+ for (const path of context.pathCandidates) {
2119
+ const candidateSkillName = context.override?.path && path === context.override.path ? context.override.skillName || path.split("/").pop() || packageInfo.packageName : path.split("/").pop() || packageInfo.packageName;
1389
2120
  const resolved = await materializeCandidate(packageInfo, {
1390
2121
  skillName: candidateSkillName,
1391
2122
  sourcePath: path,
1392
2123
  sourceKind: "github",
1393
- sourceRepo: repo,
2124
+ sourceRepo: context.repo,
1394
2125
  sourceRef: ref,
1395
2126
  official: true,
1396
2127
  resolver: "githubHeuristic"
@@ -1401,39 +2132,33 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
1401
2132
  }
1402
2133
  return null;
1403
2134
  };
1404
- for (const ref of refs) {
1405
- const resolved = await tryResolveForRef(ref, false);
1406
- if (resolved?.length) {
1407
- return { contributions: resolved, issues, skipped };
1408
- }
1409
- }
1410
2135
  const fallbackRefs = dedupe([
1411
- await fetchGitHubDefaultBranch(repo, timeoutMs) || "",
2136
+ await fetchGitHubDefaultBranch(context.repo, timeoutMs) || "",
1412
2137
  "main",
1413
2138
  "master"
1414
2139
  ]);
1415
2140
  for (const ref of fallbackRefs) {
1416
- const resolved = await tryResolveForRef(ref, true);
2141
+ const resolved = await tryResolveForRef(ref);
1417
2142
  if (resolved?.length) {
1418
- return { contributions: resolved, issues, skipped };
2143
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
1419
2144
  }
1420
2145
  }
1421
- for (const ref of refs) {
1422
- const resolved = await tryResolveForRef(ref, true);
2146
+ for (const ref of context.refs) {
2147
+ const resolved = await tryResolveForRef(ref);
1423
2148
  if (resolved?.length) {
1424
- return { contributions: resolved, issues, skipped };
2149
+ return { contributions: resolved, issues: context.issues, skipped: context.skipped };
1425
2150
  }
1426
2151
  }
1427
- skipped.push(makeSkip(packageInfo.packageName, override?.skillName || packageInfo.packageName, "No GitHub skill found using configured refs and path heuristics", "github"));
2152
+ context.skipped.push(makeSkip(packageInfo.packageName, context.override?.skillName || packageInfo.packageName, "No GitHub skill found using configured refs and path heuristics", "github"));
1428
2153
  return {
1429
2154
  contributions: [],
1430
- issues,
1431
- skipped
2155
+ issues: context.issues,
2156
+ skipped: context.skipped
1432
2157
  };
1433
2158
  }
1434
2159
  async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
1435
2160
  const { repoUrl, sourceRepo } = resolveRepositoryUrl(packageInfo);
1436
- const docsUrl = normalizeHttpUrl(packageInfo.homepage);
2161
+ const docsUrl = resolveDocsUrl(packageInfo);
1437
2162
  if (!repoUrl && !docsUrl) {
1438
2163
  return {
1439
2164
  contributions: [],
@@ -1505,34 +2230,57 @@ async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
1505
2230
  };
1506
2231
  }
1507
2232
  async function resolveRemoteContributionsForPackage(packageInfo, options) {
1508
- const githubResult = options.enableGithubLookup ? await resolveViaGitHub(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
2233
+ const githubAgentsPromise = options.enableGithubLookup ? resolveViaGitHubAgents(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
1509
2234
  contributions: [],
1510
2235
  issues: [],
1511
2236
  skipped: []
1512
2237
  };
1513
- if (githubResult.contributions.length) {
1514
- return githubResult;
2238
+ const wellKnownPromise = options.enableWellKnownLookup === false ? {
2239
+ contributions: [],
2240
+ issues: [],
2241
+ skipped: []
2242
+ } : resolveViaWellKnown(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs, options.wellKnownLimits);
2243
+ const [githubAgentsResult, wellKnownResult] = await Promise.all([
2244
+ githubAgentsPromise,
2245
+ wellKnownPromise
2246
+ ]);
2247
+ if (githubAgentsResult.contributions.length) {
2248
+ return githubAgentsResult;
2249
+ }
2250
+ if (wellKnownResult.contributions.length) {
2251
+ return {
2252
+ contributions: wellKnownResult.contributions,
2253
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues],
2254
+ skipped: wellKnownResult.skipped
2255
+ };
2256
+ }
2257
+ const githubHeuristicResult = options.enableGithubLookup && options.enableGithubHeuristics ? await resolveViaGitHubHeuristics(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
2258
+ contributions: [],
2259
+ issues: [],
2260
+ skipped: []
2261
+ };
2262
+ if (githubHeuristicResult.contributions.length) {
2263
+ return {
2264
+ contributions: githubHeuristicResult.contributions,
2265
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues, ...githubHeuristicResult.issues],
2266
+ skipped: wellKnownResult.skipped
2267
+ };
1515
2268
  }
1516
2269
  const generatedResult = await resolveViaMetadataRouter(packageInfo, options.cacheRoot);
1517
2270
  if (generatedResult.contributions.length) {
1518
2271
  return {
1519
2272
  contributions: generatedResult.contributions,
1520
- issues: [...githubResult.issues, ...generatedResult.issues],
1521
- skipped: generatedResult.skipped
2273
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues, ...githubHeuristicResult.issues, ...generatedResult.issues],
2274
+ skipped: [...wellKnownResult.skipped, ...generatedResult.skipped]
1522
2275
  };
1523
2276
  }
1524
2277
  return {
1525
2278
  contributions: [],
1526
- issues: [...githubResult.issues, ...generatedResult.issues],
1527
- skipped: [...githubResult.skipped, ...generatedResult.skipped]
2279
+ issues: [...githubAgentsResult.issues, ...wellKnownResult.issues, ...githubHeuristicResult.issues, ...generatedResult.issues],
2280
+ skipped: [...wellKnownResult.skipped, ...githubHeuristicResult.skipped, ...generatedResult.skipped]
1528
2281
  };
1529
2282
  }
1530
2283
 
1531
- const DEFAULT_SKILL_HUB_GENERATION_MODE = "prepare";
1532
- function normalizeGenerationMode(value) {
1533
- return value === "manual" ? "manual" : DEFAULT_SKILL_HUB_GENERATION_MODE;
1534
- }
1535
-
1536
2284
  async function resolveLockfileInfo(exportRoot) {
1537
2285
  let lockfilePath;
1538
2286
  try {
@@ -1569,7 +2317,8 @@ async function createGenerationFingerprint(input) {
1569
2317
  skillName: input.options.skillName?.trim() || null,
1570
2318
  targets: [...input.options.targets || []].sort(),
1571
2319
  moduleAuthoring: Boolean(input.options.moduleAuthoring),
1572
- generationMode: normalizeGenerationMode(input.options.generationMode)
2320
+ generationMode: normalizeGenerationMode(input.options.generationMode),
2321
+ remote: normalizeRemoteOptions(input.options.remote)
1573
2322
  }
1574
2323
  });
1575
2324
  }
@@ -1582,6 +2331,7 @@ async function hashFileIfExists(path) {
1582
2331
  }
1583
2332
  }
1584
2333
 
2334
+ const HASH_READ_CONCURRENCY = 16;
1585
2335
  async function resolveManualContribution(rootDir, contribution) {
1586
2336
  const sourceDir = isAbsolute(contribution.sourceDir) ? contribution.sourceDir : resolve(rootDir, contribution.sourceDir);
1587
2337
  return normalizeContribution(contribution, sourceDir, sourceDir);
@@ -1633,8 +2383,6 @@ async function hashDirectoryTreeIfExists(rootPath) {
1633
2383
  if (!rootStat.isDirectory()) {
1634
2384
  return null;
1635
2385
  }
1636
- const hash = createHash("sha256");
1637
- hash.update("dir:.\n");
1638
2386
  const entries = await glob("**/*", {
1639
2387
  cwd: rootPath,
1640
2388
  dot: true,
@@ -1643,30 +2391,20 @@ async function hashDirectoryTreeIfExists(rootPath) {
1643
2391
  onlyFiles: false
1644
2392
  });
1645
2393
  entries.sort((a, b) => a.localeCompare(b));
1646
- for (const entry of entries) {
2394
+ const chunkGroups = await mapWithConcurrency(entries, HASH_READ_CONCURRENCY, async (entry) => {
1647
2395
  const currentPath = join(rootPath, entry);
1648
2396
  const stat = await promises.lstat(currentPath);
1649
- const relativePath = relative(rootPath, currentPath).replaceAll("\\", "/");
1650
- if (stat.isSymbolicLink()) {
1651
- hash.update(`symlink:${relativePath}:`);
1652
- hash.update(await promises.readlink(currentPath));
1653
- hash.update("\n");
1654
- continue;
1655
- }
1656
- if (stat.isDirectory()) {
1657
- hash.update(`dir:${relativePath}
1658
- `);
1659
- continue;
1660
- }
1661
- if (stat.isFile()) {
1662
- hash.update(`file:${relativePath}:`);
1663
- hash.update(await promises.readFile(currentPath));
1664
- hash.update("\n");
1665
- continue;
1666
- }
1667
- hash.update(`other:${relativePath}:${stat.mode}
1668
- `);
1669
- }
2397
+ const rel = relative(rootPath, currentPath).replaceAll("\\", "/");
2398
+ if (stat.isSymbolicLink()) return [`symlink:${rel}:`, await promises.readlink(currentPath), "\n"];
2399
+ if (stat.isDirectory()) return [`dir:${rel}
2400
+ `];
2401
+ if (stat.isFile()) return [`file:${rel}:`, await promises.readFile(currentPath), "\n"];
2402
+ return [`other:${rel}:${stat.mode}
2403
+ `];
2404
+ });
2405
+ const hash = createHash("sha256");
2406
+ hash.update("dir:.\n");
2407
+ for (const chunks of chunkGroups) for (const chunk of chunks) hash.update(chunk);
1670
2408
  return hash.digest("hex");
1671
2409
  }
1672
2410
 
@@ -1694,16 +2432,29 @@ async function writeGenerationState(generatedSkillRoot, fingerprint) {
1694
2432
  await writeFileIfChanged(getGenerationStatePath(generatedSkillRoot), `${JSON.stringify(state, null, 2)}
1695
2433
  `);
1696
2434
  }
1697
- async function isGeneratedSkillFresh(generatedSkillRoot, fingerprint) {
2435
+ async function isGeneratedSkillFreshWithOptions(generatedSkillRoot, fingerprint, options = {}) {
2436
+ if (options.refresh) {
2437
+ return false;
2438
+ }
1698
2439
  const [state, entryExists] = await Promise.all([
1699
2440
  readGenerationState(generatedSkillRoot),
1700
2441
  pathExists(join(generatedSkillRoot, "SKILL.md"))
1701
2442
  ]);
1702
- return Boolean(entryExists && state?.fingerprint === fingerprint);
2443
+ if (!entryExists || state?.fingerprint !== fingerprint) {
2444
+ return false;
2445
+ }
2446
+ if (typeof options.cacheTtlMs === "number" && Number.isFinite(options.cacheTtlMs)) {
2447
+ if (options.cacheTtlMs <= 0) {
2448
+ return false;
2449
+ }
2450
+ const generatedAt = Date.parse(state.generatedAt);
2451
+ if (!Number.isFinite(generatedAt) || Date.now() - generatedAt > options.cacheTtlMs) {
2452
+ return false;
2453
+ }
2454
+ }
2455
+ return true;
1703
2456
  }
1704
2457
 
1705
- const GITHUB_LOOKUP_TIMEOUT_MS = 1500;
1706
- const REMOTE_RESOLUTION_CONCURRENCY = 4;
1707
2458
  function resolveStableSkillBuildDir(rootDir, buildDir) {
1708
2459
  const rel = relative(resolve(rootDir), resolve(buildDir)).replaceAll("\\", "/");
1709
2460
  if (rel === ".nuxt" || rel.startsWith(".nuxt/")) {
@@ -1727,23 +2478,30 @@ function getPersistentCacheRoot(exportRoot) {
1727
2478
  function getCachedGeneratedSkillRoot(cacheRoot, skillName) {
1728
2479
  return join(cacheRoot, "generated", skillName);
1729
2480
  }
1730
- async function mapWithConcurrency(items, concurrency, mapper) {
1731
- if (!items.length) {
1732
- return [];
2481
+ function roundMs(value) {
2482
+ return Math.round(value * 10) / 10;
2483
+ }
2484
+ function formatMs(value) {
2485
+ return value >= 1e3 ? `${(value / 1e3).toFixed(1)}s` : `${Math.round(value)}ms`;
2486
+ }
2487
+ function shouldLogTimings(nuxt, explicit) {
2488
+ return explicit || Boolean(nuxt.options.debug);
2489
+ }
2490
+ function startPerfPhase(nuxt, name) {
2491
+ nuxt._perf?.startPhase(name);
2492
+ }
2493
+ function endPerfPhase(nuxt, name) {
2494
+ nuxt._perf?.endPhase(name);
2495
+ }
2496
+ function countResolvers(contributions) {
2497
+ const counts = {};
2498
+ for (const contribution of contributions) {
2499
+ counts[contribution.resolver] = (counts[contribution.resolver] || 0) + 1;
1733
2500
  }
1734
- const results = [];
1735
- let nextIndex = 0;
1736
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
1737
- while (true) {
1738
- const index = nextIndex++;
1739
- if (index >= items.length) {
1740
- return;
1741
- }
1742
- results[index] = await mapper(items[index], index);
1743
- }
1744
- });
1745
- await Promise.all(workers);
1746
- return results;
2501
+ return counts;
2502
+ }
2503
+ function formatResolverCounts(counts) {
2504
+ return Object.entries(counts).map(([resolver, count]) => `${count} ${resolver}`).join(", ");
1747
2505
  }
1748
2506
  function mergeSkipped(entries, keyFn) {
1749
2507
  const byKey = /* @__PURE__ */ new Map();
@@ -1760,6 +2518,10 @@ function mergeSkipped(entries, keyFn) {
1760
2518
  }
1761
2519
  return Array.from(byKey.values()).sort((a, b) => `${a.packageName}::${a.skillName}`.localeCompare(`${b.packageName}::${b.skillName}`));
1762
2520
  }
2521
+ function filterResolvedSkipped(entries, contributions) {
2522
+ const resolvedKeys = new Set(contributions.map((contribution) => `${contribution.packageName}::${contribution.skillName}`));
2523
+ return entries.filter((entry) => !resolvedKeys.has(`${entry.packageName}::${entry.skillName}`));
2524
+ }
1763
2525
  async function removeLegacyRootArtifacts(skillRoot) {
1764
2526
  await Promise.all([
1765
2527
  promises.rm(join(skillRoot, "references"), { recursive: true, force: true }),
@@ -1781,17 +2543,13 @@ async function collectInstalledPackages(nuxt) {
1781
2543
  discoveries.push(discovered);
1782
2544
  }
1783
2545
  };
1784
- const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
1785
- const seenSpecifiers = Array.from(new Set(moduleSpecifiers));
1786
- for (const specifier of seenSpecifiers) {
1787
- addInstalledPackage(await discoverInstalledPackageFromSpecifier(specifier, nuxt.options.rootDir));
1788
- }
2546
+ const modulePackages = await collectInstalledModulePackages(nuxt.options.modules, nuxt.options.rootDir);
2547
+ for (const pkg of modulePackages) addInstalledPackage(pkg);
1789
2548
  const layerDirectories = Array.from(new Set(
1790
2549
  nuxt.options._layers.map((layer) => resolve(layer.cwd || layer.config.rootDir || "")).filter((layerDir) => layerDir && layerDir !== resolve(nuxt.options.rootDir))
1791
2550
  ));
1792
- for (const layerDirectory of layerDirectories) {
1793
- addInstalledPackage(await discoverInstalledPackageFromDirectory(layerDirectory));
1794
- }
2551
+ const layerPackages = await Promise.all(layerDirectories.map((dir) => discoverInstalledPackageFromDirectory(dir)));
2552
+ for (const pkg of layerPackages) addInstalledPackage(pkg);
1795
2553
  const localPackageSkills = await discoverLocalPackageSkills(nuxt.options.rootDir);
1796
2554
  if (localPackageSkills) {
1797
2555
  discoveries.push(localPackageSkills);
@@ -1830,6 +2588,7 @@ async function generateSkillTree(input) {
1830
2588
  const modulesRoot = join(referencesRoot, "modules");
1831
2589
  const monorepoScopePath = resolveMonorepoScopePath(nuxt.options.rootDir, exportRoot);
1832
2590
  const manualContributions = [];
2591
+ const remoteOptions = normalizeRemoteOptions(options.remote);
1833
2592
  const contributionContext = {
1834
2593
  add: (contribution) => {
1835
2594
  manualContributions.push(contribution);
@@ -1837,6 +2596,9 @@ async function generateSkillTree(input) {
1837
2596
  };
1838
2597
  const callSkillContributeHook = nuxt.callHook;
1839
2598
  await callSkillContributeHook("skill-hub:contribute", contributionContext);
2599
+ const callResolveStartHook = nuxt.callHook;
2600
+ const callResolvePackageHook = nuxt.callHook;
2601
+ const callResolveDoneHook = nuxt.callHook;
1840
2602
  const { discoveries, installedPackages } = await collectInstalledPackages(nuxt);
1841
2603
  const discoveredContributions = await resolveContributions(discoveries);
1842
2604
  const workspaceDiscoverySources = collectWorkspaceDiscoverySources(nuxt.options.rootDir, discoveries);
@@ -1856,7 +2618,10 @@ async function generateSkillTree(input) {
1856
2618
  ...sortAndDedupeContributions(resolvedManual)
1857
2619
  ])
1858
2620
  });
1859
- if (generationMode === "prepare" && await isGeneratedSkillFresh(cachedGeneratedSkillRoot, fingerprint)) {
2621
+ if (generationMode === "prepare" && await isGeneratedSkillFreshWithOptions(cachedGeneratedSkillRoot, fingerprint, {
2622
+ refresh: remoteOptions.refresh,
2623
+ cacheTtlMs: remoteOptions.cacheTtlMs
2624
+ })) {
1860
2625
  await copySkillTree(cachedGeneratedSkillRoot, generatedSkillRoot, true);
1861
2626
  logger.info(`Restored cached ${skillName} skill from ${cachedGeneratedSkillRoot}`);
1862
2627
  return;
@@ -1869,23 +2634,89 @@ async function generateSkillTree(input) {
1869
2634
  await ensureDir(remoteCacheRoot);
1870
2635
  const packagesToResolve = installedPackages.filter((pkg) => pkg.packageName !== "nuxt-skill-hub" && !distResolvedPackages.has(pkg.packageName));
1871
2636
  const totalToResolve = packagesToResolve.length;
1872
- const remoteResults = await mapWithConcurrency(
1873
- packagesToResolve,
1874
- REMOTE_RESOLUTION_CONCURRENCY,
1875
- async (pkg, index) => {
1876
- logger.start(`Resolving skills for ${pkg.packageName} (${index + 1}/${totalToResolve})...`);
1877
- return await resolveRemoteContributionsForPackage(pkg, {
1878
- cacheRoot: remoteCacheRoot,
1879
- githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
1880
- enableGithubLookup: true
1881
- });
2637
+ const remoteStart = performance.now();
2638
+ const logTimings = shouldLogTimings(nuxt, remoteOptions.timings);
2639
+ await callResolveStartHook("skill-hub:resolve:start", {
2640
+ skillName,
2641
+ packageCount: totalToResolve,
2642
+ concurrency: remoteOptions.concurrency,
2643
+ remoteEnabled: remoteOptions.enabled,
2644
+ githubHeuristics: remoteOptions.githubHeuristics
2645
+ });
2646
+ startPerfPhase(nuxt, "skill-hub:resolve");
2647
+ const remoteResults = await (async () => {
2648
+ try {
2649
+ return await mapWithConcurrency(
2650
+ packagesToResolve,
2651
+ remoteOptions.concurrency,
2652
+ async (pkg, index) => {
2653
+ logger.start(`Resolving skills for ${pkg.packageName} (${index + 1}/${totalToResolve})...`);
2654
+ const packageStart = performance.now();
2655
+ const phaseName = `skill-hub:resolve:${pkg.packageName}`;
2656
+ startPerfPhase(nuxt, phaseName);
2657
+ try {
2658
+ const result = await resolveRemoteContributionsForPackage(pkg, {
2659
+ cacheRoot: remoteCacheRoot,
2660
+ githubLookupTimeoutMs: remoteOptions.timeoutMs,
2661
+ enableGithubLookup: remoteOptions.enabled,
2662
+ enableWellKnownLookup: remoteOptions.enabled,
2663
+ enableGithubHeuristics: remoteOptions.enabled && remoteOptions.githubHeuristics,
2664
+ wellKnownLimits: {
2665
+ maxFiles: remoteOptions.maxFiles,
2666
+ maxBytes: remoteOptions.maxBytes,
2667
+ maxFileBytes: remoteOptions.maxFileBytes
2668
+ }
2669
+ });
2670
+ const durationMs = roundMs(performance.now() - packageStart);
2671
+ const firstContribution = result.contributions[0];
2672
+ await callResolvePackageHook("skill-hub:resolve:package", {
2673
+ skillName,
2674
+ packageName: pkg.packageName,
2675
+ packageIndex: index,
2676
+ packageCount: totalToResolve,
2677
+ durationMs,
2678
+ resolver: firstContribution?.resolver,
2679
+ sourceKind: firstContribution?.sourceKind,
2680
+ contributionCount: result.contributions.length,
2681
+ skippedCount: result.skipped.length
2682
+ });
2683
+ if (logTimings) {
2684
+ const outcome = firstContribution ? `${firstContribution.resolver}/${firstContribution.sourceKind}` : "unresolved";
2685
+ logger.info(`Resolved ${pkg.packageName} via ${outcome} in ${formatMs(durationMs)}`);
2686
+ }
2687
+ return result;
2688
+ } finally {
2689
+ endPerfPhase(nuxt, phaseName);
2690
+ }
2691
+ }
2692
+ );
2693
+ } finally {
2694
+ endPerfPhase(nuxt, "skill-hub:resolve");
1882
2695
  }
1883
- );
2696
+ })();
1884
2697
  for (const remote of remoteResults) {
1885
2698
  remoteIssues.push(...remote.issues);
1886
2699
  remoteSkipped.push(...remote.skipped);
1887
2700
  remoteContributions.push(...remote.contributions);
1888
2701
  }
2702
+ const remoteDurationMs = roundMs(performance.now() - remoteStart);
2703
+ const resolverCounts = countResolvers(remoteContributions);
2704
+ await callResolveDoneHook("skill-hub:resolve:done", {
2705
+ skillName,
2706
+ packageCount: totalToResolve,
2707
+ concurrency: remoteOptions.concurrency,
2708
+ remoteEnabled: remoteOptions.enabled,
2709
+ githubHeuristics: remoteOptions.githubHeuristics,
2710
+ durationMs: remoteDurationMs,
2711
+ contributionCount: remoteContributions.length,
2712
+ skippedCount: remoteSkipped.length,
2713
+ issueCount: remoteIssues.length,
2714
+ resolverCounts
2715
+ });
2716
+ if (totalToResolve) {
2717
+ const summary = formatResolverCounts(resolverCounts);
2718
+ logger.info(`Resolved ${totalToResolve} module skill sources in ${formatMs(remoteDurationMs)}${summary ? ` (${summary})` : ""}.`);
2719
+ }
1889
2720
  const validatedRemote = await validateResolvedContributions(sortAndDedupeContributions(remoteContributions));
1890
2721
  const validatedManual = await validateResolvedContributions(sortAndDedupeContributions(resolvedManual));
1891
2722
  const contributions = sortAndDedupeContributions([
@@ -1903,10 +2734,10 @@ async function generateSkillTree(input) {
1903
2734
  validationIssues.map((issue) => ({ packageName: issue.packageName, skillName: issue.skillName, reason: issue.reason, sourceKind: issue.sourceKind })),
1904
2735
  (entry) => `${entry.packageName}::${entry.skillName}`
1905
2736
  );
1906
- const skipped = mergeSkipped(
2737
+ const skipped = filterResolvedSkipped(mergeSkipped(
1907
2738
  [...issuesToSkipped, ...remoteSkipped],
1908
2739
  (entry) => `${entry.packageName}::${entry.skillName}::${entry.sourceKind || ""}`
1909
- );
2740
+ ), contributions);
1910
2741
  for (const issue of validationIssues) {
1911
2742
  logger.warn(`[validation] ${issue.packageName}/${issue.skillName}: ${issue.reason}`);
1912
2743
  }
@@ -1954,6 +2785,10 @@ async function generateSkillTree(input) {
1954
2785
  resolver: contribution.resolver
1955
2786
  });
1956
2787
  }
2788
+ const moduleIndex = createModuleIndex(generatedEntries, skipped);
2789
+ if (moduleIndex) {
2790
+ await writeFileIfChanged(join(modulesRoot, "index.md"), moduleIndex);
2791
+ }
1957
2792
  const nuxtMetadata = await loadNuxtMetadata();
1958
2793
  await writeFileIfChanged(join(generatedSkillRoot, "SKILL.md"), createSkillEntrypoint(
1959
2794
  skillName,
@@ -1971,24 +2806,31 @@ async function generateSkillTree(input) {
1971
2806
  function isCancelled(value) {
1972
2807
  return value === null || typeof value === "symbol";
1973
2808
  }
1974
- const AI_AGENT_TARGET_MAP = {
1975
- codex: "codex",
1976
- claude: "claude-code",
1977
- gemini: "gemini-code-assist",
1978
- replit: "replit-agent"
1979
- };
1980
- function resolveAIGuidanceTarget(currentTarget, detectedAgent) {
1981
- if (currentTarget) {
1982
- return currentTarget;
1983
- }
1984
- if (!detectedAgent) {
1985
- return void 0;
2809
+ function normalizeTargetSelection(value) {
2810
+ if (!Array.isArray(value)) {
2811
+ return [];
1986
2812
  }
1987
- return AI_AGENT_TARGET_MAP[detectedAgent];
1988
- }
1989
- function buildAIGuidance(currentTarget, detectedAgent) {
1990
- const target = resolveAIGuidanceTarget(currentTarget, detectedAgent);
2813
+ return value.map((entry) => {
2814
+ if (typeof entry === "string") return entry;
2815
+ if (entry && typeof entry === "object" && typeof entry.value === "string") {
2816
+ return entry.value;
2817
+ }
2818
+ return "";
2819
+ }).filter(Boolean);
2820
+ }
2821
+ function buildAIGuidance(currentTarget, detectedAgent, skillName = "nuxt") {
2822
+ const agentTargetMap = {
2823
+ codex: "codex",
2824
+ claude: "claude-code",
2825
+ gemini: "gemini-code-assist",
2826
+ replit: "replit-agent"
2827
+ };
2828
+ const target = currentTarget || (detectedAgent ? agentTargetMap[detectedAgent] : void 0);
1991
2829
  const sourceLabel = detectedAgent ? ` (${detectedAgent})` : "";
2830
+ const hintFile = target === "claude-code" ? "CLAUDE.md" : "AGENTS.md";
2831
+ const hint = `${MANAGED_HINT_START}
2832
+ Use the \`${skillName}\` skill as the Nuxt router/entrypoint for tasks in this repository.
2833
+ ${MANAGED_HINT_END}`;
1992
2834
  const snippetLines = [
1993
2835
  "export default defineNuxtConfig({",
1994
2836
  " skillHub: {",
@@ -2000,17 +2842,25 @@ function buildAIGuidance(currentTarget, detectedAgent) {
2000
2842
  return {
2001
2843
  intro: `Install wizard skipped because nuxt-skill-hub was installed by an AI agent${sourceLabel}. Add this to your nuxt.config.ts:`,
2002
2844
  snippet: snippetLines.join("\n"),
2845
+ hintFile,
2846
+ hint,
2847
+ nextStep: `Add the managed hint to ${hintFile}, then run \`npx nuxi prepare\` to generate the Nuxt skill tree.`,
2003
2848
  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."
2004
2849
  };
2005
2850
  }
2006
2851
  async function runInstallWizard(nuxt) {
2007
2852
  const logger = useLogger("nuxt-skill-hub");
2008
2853
  if (isAgent) {
2009
- const guidance = buildAIGuidance(detectCurrentTarget(), agent);
2854
+ const guidance = buildAIGuidance(detectCurrentTarget(), agent, await deriveSkillName(nuxt.options.rootDir));
2010
2855
  logger.info(guidance.intro);
2011
2856
  logger.info(`\`\`\`ts
2012
2857
  ${guidance.snippet}
2013
2858
  \`\`\``);
2859
+ logger.info(`Add this to ${guidance.hintFile}:
2860
+ \`\`\`md
2861
+ ${guidance.hint}
2862
+ \`\`\``);
2863
+ logger.info(guidance.nextStep);
2014
2864
  logger.info(guidance.optionalFlags);
2015
2865
  return;
2016
2866
  }
@@ -2042,6 +2892,20 @@ Let's configure AI agent skills for your Nuxt project.`
2042
2892
  }
2043
2893
  consola.info(`Detected agents: ${detectedTargets.length ? detectedTargets.join(", ") : "none"}`);
2044
2894
  selectedTargets = detectedTargets;
2895
+ if (!selectedTargets.length) {
2896
+ const chosen = await consola.prompt("Select agents to generate skills for:", {
2897
+ type: "multiselect",
2898
+ options: allTargets.map((t) => ({ label: t, value: t })),
2899
+ initial: allTargets.includes("codex") ? ["codex"] : allTargets.slice(0, 1),
2900
+ required: true,
2901
+ cancel: "null"
2902
+ });
2903
+ if (isCancelled(chosen)) {
2904
+ consola.info("Setup cancelled.");
2905
+ return;
2906
+ }
2907
+ selectedTargets = normalizeTargetSelection(chosen);
2908
+ }
2045
2909
  if (detectedTargets.length > 1) {
2046
2910
  const keepAll = await consola.prompt("Generate skills for all detected agents?", {
2047
2911
  type: "confirm",
@@ -2064,19 +2928,11 @@ Let's configure AI agent skills for your Nuxt project.`
2064
2928
  consola.info("Setup cancelled.");
2065
2929
  return;
2066
2930
  }
2067
- selectedTargets = chosen.map((c) => c.value);
2931
+ selectedTargets = normalizeTargetSelection(chosen);
2068
2932
  }
2069
2933
  }
2070
2934
  }
2071
- const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
2072
- const uniqueSpecifiers = Array.from(new Set(moduleSpecifiers));
2073
- const installedModulePackages = [];
2074
- for (const specifier of uniqueSpecifiers) {
2075
- const pkg = await discoverInstalledPackageFromSpecifier(specifier, rootDir);
2076
- if (!pkg || pkg.packageName === "nuxt-skill-hub")
2077
- continue;
2078
- installedModulePackages.push(pkg.packageName);
2079
- }
2935
+ const installedModulePackages = (await collectInstalledModulePackages(nuxt.options.modules, rootDir)).map((pkg) => pkg.packageName).filter((name) => name !== "nuxt-skill-hub");
2080
2936
  if (installedModulePackages.length) {
2081
2937
  consola.info(`Detected ${installedModulePackages.length} installed module package(s):`);
2082
2938
  for (const packageName of installedModulePackages) {