nuxt-skill-hub 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,10 +22,20 @@
22
22
 
23
23
  ## Install
24
24
 
25
+ For agents:
26
+
27
+ ```bash
28
+ npx skills add https://nuxt-skill.onmax.me/
29
+ ```
30
+
31
+ For Nuxt projects:
32
+
25
33
  ```bash
26
- npx nuxt module add nuxt-skill-hub
34
+ npx nuxi module add nuxt-skill-hub
27
35
  ```
28
36
 
37
+ Use [Doctor](https://vite-doctor.onmax.me/) to check AI-written Nuxt changes for Nuxt, Vue, Nitro, and Vite issues before review.
38
+
29
39
  ## License
30
40
 
31
41
  MIT
package/dist/module.d.mts CHANGED
@@ -3,19 +3,52 @@ import * as _nuxt_schema from '@nuxt/schema';
3
3
  type SkillHubTarget = string;
4
4
 
5
5
  type SkillSourceKind = 'dist' | 'github' | 'wellKnown' | 'generated';
6
- type SkillResolverKind = 'agentsField' | 'githubHeuristic' | 'wellKnownV2' | 'metadataRouter';
6
+ type SkillResolverKind = 'agentsField' | 'githubHeuristic' | 'wellKnownV2' | 'wellKnownSkills' | 'metadataRouter';
7
7
 
8
8
  type SkillHubGenerationMode = 'prepare' | 'manual';
9
+ interface SkillHubRemoteOptions {
10
+ enabled?: boolean;
11
+ timeoutMs?: number;
12
+ concurrency?: number;
13
+ refresh?: boolean;
14
+ cacheTtlMs?: number;
15
+ githubHeuristics?: boolean;
16
+ timings?: boolean;
17
+ maxFiles?: number;
18
+ maxBytes?: number;
19
+ maxFileBytes?: number;
20
+ }
21
+ interface SkillHubResolveStartContext {
22
+ skillName: string;
23
+ packageCount: number;
24
+ concurrency: number;
25
+ remoteEnabled: boolean;
26
+ githubHeuristics: boolean;
27
+ }
28
+ interface SkillHubResolvePackageContext {
29
+ skillName: string;
30
+ packageName: string;
31
+ packageIndex: number;
32
+ packageCount: number;
33
+ durationMs: number;
34
+ resolver?: SkillResolverKind;
35
+ sourceKind?: SkillSourceKind;
36
+ contributionCount: number;
37
+ skippedCount: number;
38
+ }
39
+ interface SkillHubResolveDoneContext extends SkillHubResolveStartContext {
40
+ durationMs: number;
41
+ contributionCount: number;
42
+ skippedCount: number;
43
+ issueCount: number;
44
+ resolverCounts: Partial<Record<SkillResolverKind, number>>;
45
+ }
9
46
  interface ModuleOptions {
10
47
  skillName?: string;
11
48
  targets?: SkillHubTarget[];
12
49
  moduleAuthoring?: boolean;
13
50
  generationMode?: SkillHubGenerationMode;
14
- /**
15
- * Auto-register ESLint rule to remove redundant imports when @nuxt/eslint is installed.
16
- * @default true
17
- */
18
- eslint?: boolean;
51
+ remote?: boolean | SkillHubRemoteOptions;
19
52
  }
20
53
  interface SkillHubContribution {
21
54
  packageName: string;
@@ -42,6 +75,9 @@ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, fa
42
75
  declare module '@nuxt/schema' {
43
76
  interface NuxtHooks {
44
77
  'skill-hub:contribute': (ctx: SkillHubContributionContext) => void | Promise<void>;
78
+ 'skill-hub:resolve:start': (ctx: SkillHubResolveStartContext) => void | Promise<void>;
79
+ 'skill-hub:resolve:package': (ctx: SkillHubResolvePackageContext) => void | Promise<void>;
80
+ 'skill-hub:resolve:done': (ctx: SkillHubResolveDoneContext) => void | Promise<void>;
45
81
  }
46
82
  }
47
83
 
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-skill-hub",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
4
4
  "configKey": "skillHub",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.3.0"
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';
@@ -17,7 +18,7 @@ import { glob } from 'tinyglobby';
17
18
  import { createConsola } from 'consola';
18
19
  import { colorize } from 'consola/utils';
19
20
 
20
- const version = "0.0.8";
21
+ const version = "0.1.1";
21
22
  const packageJson = {
22
23
  version: version};
23
24
 
@@ -178,14 +179,14 @@ function resolveMetadataRouterSkillName(packageName) {
178
179
  }
179
180
  function createMetadataRouterSkillFiles(input) {
180
181
  const description = input.description?.trim() || `Metadata-routed module skill for ${input.packageName}. Use it to reach the official docs and upstream source.`;
181
- const linkLines = createCompactMetadataRouterContent(input).trim();
182
+ const body = createMetadataRouterContent(input).trim();
182
183
  return {
183
184
  "SKILL.md": `---
184
185
  name: ${yamlString(input.skillName)}
185
186
  description: ${yamlString(description)}
186
187
  ---
187
188
 
188
- ${linkLines || "Docs: unavailable\nSource code: unavailable"}
189
+ ${body}
189
190
  `
190
191
  };
191
192
  }
@@ -245,6 +246,43 @@ function renderSkippedEntries(skipped) {
245
246
  ${lines.join("\n")}
246
247
  `;
247
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
+ }
248
286
  function findPack(metadata, id) {
249
287
  return metadata.packs.find((pack) => pack.id === id) || metadata.packs[0];
250
288
  }
@@ -329,25 +367,12 @@ function createRoutingExamples(metadata, includeModuleAuthoring = false) {
329
367
  ${lines.join("\n")}`;
330
368
  }
331
369
  function createSkillMapSections(metadata, entries, skipped = [], includeModuleAuthoring = false) {
332
- const includeModuleSections = hasModuleGuidance(entries, skipped);
333
- const grouped = groupModuleEntries(entries);
334
- const moduleSections = [
335
- renderModuleGroup("Official upstream skills", grouped.officialUpstream, "./references/modules/"),
336
- renderModuleGroup("Resolved module skills", grouped.githubResolved, "./references/modules/"),
337
- renderModuleGroup("Docs-discovered skills", grouped.wellKnownResolved, "./references/modules/"),
338
- renderModuleGroup("Metadata-routed skills", grouped.metadataRouted, "./references/modules/"),
339
- renderSkippedEntries(skipped)
340
- ].filter(Boolean);
341
- const moduleGuidesSection = includeModuleSections ? `## Module guides
342
- 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.
343
-
344
- ${moduleSections.join("\n")}` : "";
345
370
  return [
346
371
  "## Routing",
347
372
  "Load one matching guide first. Open more guides only when the first guide points you there or the task clearly crosses boundaries.",
348
373
  createRoutingTable(metadata, includeModuleAuthoring),
349
374
  createRoutingExamples(metadata, includeModuleAuthoring),
350
- moduleGuidesSection
375
+ renderModuleSummary(entries, skipped)
351
376
  ].filter(Boolean).join("\n\n");
352
377
  }
353
378
  function createSkillEntrypoint(skillName, metadata, monorepoScopePath, includeModuleAuthoring = false, entries = [], skipped = []) {
@@ -426,6 +451,23 @@ function createCompactMetadataRouterContent(entry) {
426
451
  })}
427
452
  `;
428
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
+ }
429
471
 
430
472
  async function loadSkillFilesFromDir(dir, skip) {
431
473
  const skipSet = skip ? new Set(skip) : void 0;
@@ -739,6 +781,22 @@ function readRepositoryUrl(repository) {
739
781
  if (repository && typeof repository.url === "string") return repository.url;
740
782
  return void 0;
741
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
+ }
742
800
  async function discoverInstalledPackageFromSpecifier(specifier, rootDir) {
743
801
  if (!isPackageSpecifier(specifier)) {
744
802
  return null;
@@ -778,6 +836,11 @@ async function collectInstalledModulePackages(modules, rootDir) {
778
836
  }
779
837
  async function readInstalledPackageMetadata(packageJsonPath, fallbackSpecifier) {
780
838
  const pkg = await readPackageJSON(packageJsonPath);
839
+ const docsUrls = dedupe$1([
840
+ ...readUrlValues(pkg.docs),
841
+ ...readUrlValues(pkg.documentation),
842
+ ...readUrlValues(pkg.homepage)
843
+ ]);
781
844
  return {
782
845
  packageName: pkg.name || (fallbackSpecifier ? packageNameFromSpecifier(fallbackSpecifier) : basename(dirname(packageJsonPath))),
783
846
  version: pkg.version,
@@ -785,6 +848,7 @@ async function readInstalledPackageMetadata(packageJsonPath, fallbackSpecifier)
785
848
  packageRoot: dirname(packageJsonPath),
786
849
  repository: readRepositoryUrl(pkg.repository),
787
850
  homepage: pkg.homepage,
851
+ docsUrls,
788
852
  packageData: pkg
789
853
  };
790
854
  }
@@ -1287,8 +1351,54 @@ async function fetchUrlBytes(url, timeoutMs) {
1287
1351
  });
1288
1352
  }
1289
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
+
1290
1390
  const WELL_KNOWN_DISCOVERY_V2_SCHEMA = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
1291
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
+ }
1292
1402
  function makeSkip$1(packageName, skillName, reason) {
1293
1403
  return {
1294
1404
  packageName,
@@ -1387,6 +1497,7 @@ function docsBaseCandidates(packageInfo) {
1387
1497
  const override = findPackageOverride(packageInfo.packageName);
1388
1498
  const candidates = [
1389
1499
  ...override?.docsUrls || [],
1500
+ ...packageInfo.docsUrls || [],
1390
1501
  packageInfo.homepage || ""
1391
1502
  ];
1392
1503
  return [...new Set(candidates.map(normalizeHttpUrl$1).filter((url) => Boolean(url)).filter(isPublicDiscoveryBase))];
@@ -1401,6 +1512,23 @@ function normalizeDigest(value) {
1401
1512
  function sha256(bytes) {
1402
1513
  return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
1403
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
+ }
1404
1532
  async function writeSkillFile(root, relativePath, bytes) {
1405
1533
  const destination = join(root, relativePath);
1406
1534
  await ensureDir(dirname(destination));
@@ -1411,6 +1539,7 @@ function contributionFor(input) {
1411
1539
  return normalizeContribution({
1412
1540
  packageName: input.packageInfo.packageName,
1413
1541
  version: input.packageInfo.version,
1542
+ sourceDir: input.targetDir,
1414
1543
  skillName: input.skillName,
1415
1544
  description: input.description,
1416
1545
  sourceKind: "wellKnown",
@@ -1423,6 +1552,90 @@ function contributionFor(input) {
1423
1552
  forceIncludeScripts: true
1424
1553
  }, input.targetDir, join(input.targetDir, ".."));
1425
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
+ }
1426
1639
  async function materializeV2Skill(input) {
1427
1640
  const skillName = typeof input.entry.name === "string" ? input.entry.name.trim() : "";
1428
1641
  if (!isValidSkillName(skillName)) {
@@ -1457,6 +1670,9 @@ async function materializeV2Skill(input) {
1457
1670
  if (sha256(response.data) !== digest) {
1458
1671
  return { skipped: makeSkip$1(input.packageInfo.packageName, skillName, "well-known v2 skill digest mismatch") };
1459
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
+ }
1460
1676
  const origin = new URL(input.docsUrl).origin;
1461
1677
  const targetDir = join(
1462
1678
  input.cacheRoot,
@@ -1481,6 +1697,44 @@ async function materializeV2Skill(input) {
1481
1697
  })
1482
1698
  };
1483
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
+ }
1484
1738
  async function resolveV2Index(input) {
1485
1739
  const skipped = [];
1486
1740
  const contributions = [];
@@ -1491,19 +1745,25 @@ async function resolveV2Index(input) {
1491
1745
  skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known v2 index is missing a skills array")]
1492
1746
  };
1493
1747
  }
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;
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
+ });
1498
1764
  }
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
- });
1765
+ );
1766
+ for (const materialized of materializedEntries) {
1507
1767
  if (materialized?.contribution) {
1508
1768
  contributions.push(materialized.contribution);
1509
1769
  }
@@ -1539,6 +1799,12 @@ async function resolveIndex(input) {
1539
1799
  index
1540
1800
  });
1541
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
+ }
1542
1808
  if (typeof index.$schema === "string" && index.$schema) {
1543
1809
  return {
1544
1810
  contributions: [],
@@ -1552,19 +1818,30 @@ async function resolveIndex(input) {
1552
1818
  skipped: [makeSkip$1(input.packageInfo.packageName, input.packageInfo.packageName, "well-known index is missing the v2 schema")]
1553
1819
  };
1554
1820
  }
1555
- async function resolveViaWellKnown(packageInfo, cacheRoot, timeoutMs) {
1821
+ async function resolveViaWellKnown(packageInfo, cacheRoot, timeoutMs, rawLimits) {
1556
1822
  const bases = docsBaseCandidates(packageInfo);
1557
1823
  const skipped = [];
1558
1824
  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
- });
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) {
1568
1845
  if (!result) {
1569
1846
  continue;
1570
1847
  }
@@ -1636,7 +1913,8 @@ function normalizeHttpUrl(url) {
1636
1913
  function resolveDocsUrl(packageInfo) {
1637
1914
  const override = findPackageOverride(packageInfo.packageName);
1638
1915
  const overrideDocsUrl = override?.docsUrls?.map(normalizeHttpUrl).find(Boolean);
1639
- return overrideDocsUrl || normalizeHttpUrl(packageInfo.homepage);
1916
+ const packageDocsUrl = packageInfo.docsUrls?.map(normalizeHttpUrl).find(Boolean);
1917
+ return overrideDocsUrl || packageDocsUrl || normalizeHttpUrl(packageInfo.homepage);
1640
1918
  }
1641
1919
  function resolveRepositoryUrl(packageInfo) {
1642
1920
  const override = findPackageOverride(packageInfo.packageName);
@@ -1952,15 +2230,23 @@ async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
1952
2230
  };
1953
2231
  }
1954
2232
  async function resolveRemoteContributionsForPackage(packageInfo, options) {
1955
- const githubAgentsResult = options.enableGithubLookup ? await resolveViaGitHubAgents(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
2233
+ const githubAgentsPromise = options.enableGithubLookup ? resolveViaGitHubAgents(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
1956
2234
  contributions: [],
1957
2235
  issues: [],
1958
2236
  skipped: []
1959
2237
  };
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
+ ]);
1960
2247
  if (githubAgentsResult.contributions.length) {
1961
2248
  return githubAgentsResult;
1962
2249
  }
1963
- const wellKnownResult = await resolveViaWellKnown(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs);
1964
2250
  if (wellKnownResult.contributions.length) {
1965
2251
  return {
1966
2252
  contributions: wellKnownResult.contributions,
@@ -1968,7 +2254,7 @@ async function resolveRemoteContributionsForPackage(packageInfo, options) {
1968
2254
  skipped: wellKnownResult.skipped
1969
2255
  };
1970
2256
  }
1971
- const githubHeuristicResult = options.enableGithubLookup ? await resolveViaGitHubHeuristics(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
2257
+ const githubHeuristicResult = options.enableGithubLookup && options.enableGithubHeuristics ? await resolveViaGitHubHeuristics(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
1972
2258
  contributions: [],
1973
2259
  issues: [],
1974
2260
  skipped: []
@@ -1995,11 +2281,6 @@ async function resolveRemoteContributionsForPackage(packageInfo, options) {
1995
2281
  };
1996
2282
  }
1997
2283
 
1998
- const DEFAULT_SKILL_HUB_GENERATION_MODE = "prepare";
1999
- function normalizeGenerationMode(value) {
2000
- return value === "manual" ? "manual" : DEFAULT_SKILL_HUB_GENERATION_MODE;
2001
- }
2002
-
2003
2284
  async function resolveLockfileInfo(exportRoot) {
2004
2285
  let lockfilePath;
2005
2286
  try {
@@ -2036,7 +2317,8 @@ async function createGenerationFingerprint(input) {
2036
2317
  skillName: input.options.skillName?.trim() || null,
2037
2318
  targets: [...input.options.targets || []].sort(),
2038
2319
  moduleAuthoring: Boolean(input.options.moduleAuthoring),
2039
- generationMode: normalizeGenerationMode(input.options.generationMode)
2320
+ generationMode: normalizeGenerationMode(input.options.generationMode),
2321
+ remote: normalizeRemoteOptions(input.options.remote)
2040
2322
  }
2041
2323
  });
2042
2324
  }
@@ -2150,16 +2432,29 @@ async function writeGenerationState(generatedSkillRoot, fingerprint) {
2150
2432
  await writeFileIfChanged(getGenerationStatePath(generatedSkillRoot), `${JSON.stringify(state, null, 2)}
2151
2433
  `);
2152
2434
  }
2153
- async function isGeneratedSkillFresh(generatedSkillRoot, fingerprint) {
2435
+ async function isGeneratedSkillFreshWithOptions(generatedSkillRoot, fingerprint, options = {}) {
2436
+ if (options.refresh) {
2437
+ return false;
2438
+ }
2154
2439
  const [state, entryExists] = await Promise.all([
2155
2440
  readGenerationState(generatedSkillRoot),
2156
2441
  pathExists(join(generatedSkillRoot, "SKILL.md"))
2157
2442
  ]);
2158
- 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;
2159
2456
  }
2160
2457
 
2161
- const GITHUB_LOOKUP_TIMEOUT_MS = 1500;
2162
- const REMOTE_RESOLUTION_CONCURRENCY = 4;
2163
2458
  function resolveStableSkillBuildDir(rootDir, buildDir) {
2164
2459
  const rel = relative(resolve(rootDir), resolve(buildDir)).replaceAll("\\", "/");
2165
2460
  if (rel === ".nuxt" || rel.startsWith(".nuxt/")) {
@@ -2183,6 +2478,31 @@ function getPersistentCacheRoot(exportRoot) {
2183
2478
  function getCachedGeneratedSkillRoot(cacheRoot, skillName) {
2184
2479
  return join(cacheRoot, "generated", skillName);
2185
2480
  }
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;
2500
+ }
2501
+ return counts;
2502
+ }
2503
+ function formatResolverCounts(counts) {
2504
+ return Object.entries(counts).map(([resolver, count]) => `${count} ${resolver}`).join(", ");
2505
+ }
2186
2506
  function mergeSkipped(entries, keyFn) {
2187
2507
  const byKey = /* @__PURE__ */ new Map();
2188
2508
  for (const entry of entries) {
@@ -2198,6 +2518,10 @@ function mergeSkipped(entries, keyFn) {
2198
2518
  }
2199
2519
  return Array.from(byKey.values()).sort((a, b) => `${a.packageName}::${a.skillName}`.localeCompare(`${b.packageName}::${b.skillName}`));
2200
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
+ }
2201
2525
  async function removeLegacyRootArtifacts(skillRoot) {
2202
2526
  await Promise.all([
2203
2527
  promises.rm(join(skillRoot, "references"), { recursive: true, force: true }),
@@ -2264,6 +2588,7 @@ async function generateSkillTree(input) {
2264
2588
  const modulesRoot = join(referencesRoot, "modules");
2265
2589
  const monorepoScopePath = resolveMonorepoScopePath(nuxt.options.rootDir, exportRoot);
2266
2590
  const manualContributions = [];
2591
+ const remoteOptions = normalizeRemoteOptions(options.remote);
2267
2592
  const contributionContext = {
2268
2593
  add: (contribution) => {
2269
2594
  manualContributions.push(contribution);
@@ -2271,6 +2596,9 @@ async function generateSkillTree(input) {
2271
2596
  };
2272
2597
  const callSkillContributeHook = nuxt.callHook;
2273
2598
  await callSkillContributeHook("skill-hub:contribute", contributionContext);
2599
+ const callResolveStartHook = nuxt.callHook;
2600
+ const callResolvePackageHook = nuxt.callHook;
2601
+ const callResolveDoneHook = nuxt.callHook;
2274
2602
  const { discoveries, installedPackages } = await collectInstalledPackages(nuxt);
2275
2603
  const discoveredContributions = await resolveContributions(discoveries);
2276
2604
  const workspaceDiscoverySources = collectWorkspaceDiscoverySources(nuxt.options.rootDir, discoveries);
@@ -2290,7 +2618,10 @@ async function generateSkillTree(input) {
2290
2618
  ...sortAndDedupeContributions(resolvedManual)
2291
2619
  ])
2292
2620
  });
2293
- if (generationMode === "prepare" && await isGeneratedSkillFresh(cachedGeneratedSkillRoot, fingerprint)) {
2621
+ if (generationMode === "prepare" && await isGeneratedSkillFreshWithOptions(cachedGeneratedSkillRoot, fingerprint, {
2622
+ refresh: remoteOptions.refresh,
2623
+ cacheTtlMs: remoteOptions.cacheTtlMs
2624
+ })) {
2294
2625
  await copySkillTree(cachedGeneratedSkillRoot, generatedSkillRoot, true);
2295
2626
  logger.info(`Restored cached ${skillName} skill from ${cachedGeneratedSkillRoot}`);
2296
2627
  return;
@@ -2303,23 +2634,89 @@ async function generateSkillTree(input) {
2303
2634
  await ensureDir(remoteCacheRoot);
2304
2635
  const packagesToResolve = installedPackages.filter((pkg) => pkg.packageName !== "nuxt-skill-hub" && !distResolvedPackages.has(pkg.packageName));
2305
2636
  const totalToResolve = packagesToResolve.length;
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
- });
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");
2316
2695
  }
2317
- );
2696
+ })();
2318
2697
  for (const remote of remoteResults) {
2319
2698
  remoteIssues.push(...remote.issues);
2320
2699
  remoteSkipped.push(...remote.skipped);
2321
2700
  remoteContributions.push(...remote.contributions);
2322
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
+ }
2323
2720
  const validatedRemote = await validateResolvedContributions(sortAndDedupeContributions(remoteContributions));
2324
2721
  const validatedManual = await validateResolvedContributions(sortAndDedupeContributions(resolvedManual));
2325
2722
  const contributions = sortAndDedupeContributions([
@@ -2337,10 +2734,10 @@ async function generateSkillTree(input) {
2337
2734
  validationIssues.map((issue) => ({ packageName: issue.packageName, skillName: issue.skillName, reason: issue.reason, sourceKind: issue.sourceKind })),
2338
2735
  (entry) => `${entry.packageName}::${entry.skillName}`
2339
2736
  );
2340
- const skipped = mergeSkipped(
2737
+ const skipped = filterResolvedSkipped(mergeSkipped(
2341
2738
  [...issuesToSkipped, ...remoteSkipped],
2342
2739
  (entry) => `${entry.packageName}::${entry.skillName}::${entry.sourceKind || ""}`
2343
- );
2740
+ ), contributions);
2344
2741
  for (const issue of validationIssues) {
2345
2742
  logger.warn(`[validation] ${issue.packageName}/${issue.skillName}: ${issue.reason}`);
2346
2743
  }
@@ -2388,6 +2785,10 @@ async function generateSkillTree(input) {
2388
2785
  resolver: contribution.resolver
2389
2786
  });
2390
2787
  }
2788
+ const moduleIndex = createModuleIndex(generatedEntries, skipped);
2789
+ if (moduleIndex) {
2790
+ await writeFileIfChanged(join(modulesRoot, "index.md"), moduleIndex);
2791
+ }
2391
2792
  const nuxtMetadata = await loadNuxtMetadata();
2392
2793
  await writeFileIfChanged(join(generatedSkillRoot, "SKILL.md"), createSkillEntrypoint(
2393
2794
  skillName,
@@ -2405,7 +2806,19 @@ async function generateSkillTree(input) {
2405
2806
  function isCancelled(value) {
2406
2807
  return value === null || typeof value === "symbol";
2407
2808
  }
2408
- function buildAIGuidance(currentTarget, detectedAgent) {
2809
+ function normalizeTargetSelection(value) {
2810
+ if (!Array.isArray(value)) {
2811
+ return [];
2812
+ }
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") {
2409
2822
  const agentTargetMap = {
2410
2823
  codex: "codex",
2411
2824
  claude: "claude-code",
@@ -2414,6 +2827,10 @@ function buildAIGuidance(currentTarget, detectedAgent) {
2414
2827
  };
2415
2828
  const target = currentTarget || (detectedAgent ? agentTargetMap[detectedAgent] : void 0);
2416
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}`;
2417
2834
  const snippetLines = [
2418
2835
  "export default defineNuxtConfig({",
2419
2836
  " skillHub: {",
@@ -2425,17 +2842,25 @@ function buildAIGuidance(currentTarget, detectedAgent) {
2425
2842
  return {
2426
2843
  intro: `Install wizard skipped because nuxt-skill-hub was installed by an AI agent${sourceLabel}. Add this to your nuxt.config.ts:`,
2427
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.`,
2428
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."
2429
2849
  };
2430
2850
  }
2431
2851
  async function runInstallWizard(nuxt) {
2432
2852
  const logger = useLogger("nuxt-skill-hub");
2433
2853
  if (isAgent) {
2434
- const guidance = buildAIGuidance(detectCurrentTarget(), agent);
2854
+ const guidance = buildAIGuidance(detectCurrentTarget(), agent, await deriveSkillName(nuxt.options.rootDir));
2435
2855
  logger.info(guidance.intro);
2436
2856
  logger.info(`\`\`\`ts
2437
2857
  ${guidance.snippet}
2438
2858
  \`\`\``);
2859
+ logger.info(`Add this to ${guidance.hintFile}:
2860
+ \`\`\`md
2861
+ ${guidance.hint}
2862
+ \`\`\``);
2863
+ logger.info(guidance.nextStep);
2439
2864
  logger.info(guidance.optionalFlags);
2440
2865
  return;
2441
2866
  }
@@ -2467,6 +2892,20 @@ Let's configure AI agent skills for your Nuxt project.`
2467
2892
  }
2468
2893
  consola.info(`Detected agents: ${detectedTargets.length ? detectedTargets.join(", ") : "none"}`);
2469
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
+ }
2470
2909
  if (detectedTargets.length > 1) {
2471
2910
  const keepAll = await consola.prompt("Generate skills for all detected agents?", {
2472
2911
  type: "confirm",
@@ -2489,7 +2928,7 @@ Let's configure AI agent skills for your Nuxt project.`
2489
2928
  consola.info("Setup cancelled.");
2490
2929
  return;
2491
2930
  }
2492
- selectedTargets = chosen.map((c) => c.value);
2931
+ selectedTargets = normalizeTargetSelection(chosen);
2493
2932
  }
2494
2933
  }
2495
2934
  }
@@ -2587,49 +3026,6 @@ Configure \`skillHub.skillName\`, \`skillHub.targets\`, or set \`skillHub.genera
2587
3026
  );
2588
3027
  }
2589
3028
 
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
-
2633
3029
  const module$1 = defineNuxtModule({
2634
3030
  meta: {
2635
3031
  name: "nuxt-skill-hub",
@@ -2650,8 +3046,6 @@ const module$1 = defineNuxtModule({
2650
3046
  },
2651
3047
  async setup(options, nuxt) {
2652
3048
  const logger = useLogger("nuxt-skill-hub");
2653
- if (options.eslint !== false)
2654
- createAutoImportAddon(nuxt);
2655
3049
  if (isCI && !isTest) {
2656
3050
  logger.info("Skipping skill generation in CI");
2657
3051
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-skill-hub",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
4
4
  "description": "Teach your AI agent the Nuxt way with best practices and module guidance.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,9 +13,6 @@
13
13
  "types": "./dist/types.d.mts",
14
14
  "import": "./dist/module.mjs",
15
15
  "default": "./dist/module.mjs"
16
- },
17
- "./eslint-plugin": {
18
- "import": "./dist/runtime/eslint/plugin.js"
19
16
  }
20
17
  },
21
18
  "main": "./dist/module.mjs",
@@ -24,37 +21,36 @@
24
21
  "nuxt-best-practices"
25
22
  ],
26
23
  "dependencies": {
27
- "@nuxt/kit": "^4.3.1",
24
+ "@nuxt/kit": "^4.4.6",
28
25
  "automd": "^0.4.3",
29
- "consola": "^3.4.0",
30
- "giget": "^3.1.2",
26
+ "consola": "^3.4.2",
27
+ "giget": "^3.2.0",
31
28
  "gray-matter": "^4.0.3",
32
- "mlly": "^1.8.1",
33
- "ofetch": "^1.5.0",
29
+ "mlly": "^1.8.2",
30
+ "ofetch": "^1.5.1",
34
31
  "ohash": "^2.0.11",
35
- "pathe": "^2.0.0",
36
- "pkg-types": "^2.3.0",
37
- "std-env": "^4.0.0",
38
- "tinyglobby": "^0.2.15",
32
+ "pathe": "^2.0.3",
33
+ "pkg-types": "^2.3.1",
34
+ "std-env": "^4.1.0",
35
+ "tinyglobby": "^0.2.17",
39
36
  "unagent": "^0.0.8"
40
37
  },
41
38
  "devDependencies": {
42
- "@nuxt/devtools": "^3.2.1",
43
- "@nuxt/eslint-config": "^1.15.1",
39
+ "@nuxt/devtools": "^3.2.4",
40
+ "@nuxt/eslint-config": "^1.15.2",
44
41
  "@nuxt/module-builder": "^1.0.2",
45
- "@nuxt/schema": "^4.3.1",
46
- "@nuxt/test-utils": "^4.0.0",
47
- "@nuxtjs/mdc": "^0.20.2",
48
- "@types/node": "latest",
49
- "@vue/test-utils": "^2.4.6",
50
- "bumpp": "^10.4.1",
51
- "eslint": "^10.0.1",
52
- "happy-dom": "^20.7.0",
53
- "nuxt": "^4.3.1",
42
+ "@nuxt/schema": "^4.4.6",
43
+ "@nuxt/test-utils": "^4.0.3",
44
+ "@types/node": "^25.9.1",
45
+ "@vue/test-utils": "^2.4.10",
46
+ "bumpp": "^11.1.0",
47
+ "eslint": "^10.4.1",
48
+ "happy-dom": "^20.9.0",
49
+ "nuxt": "^4.4.6",
54
50
  "typescript": "~5.9.3",
55
- "vitest": "^4.0.18",
56
- "vue": "^3.5.29",
57
- "vue-tsc": "^3.2.5"
51
+ "vitest": "^4.1.7",
52
+ "vue": "^3.5.35",
53
+ "vue-tsc": "^3.3.3"
58
54
  },
59
55
  "scripts": {
60
56
  "dev": "pnpm --dir docs dev",
@@ -1,2 +0,0 @@
1
- import type { Rule } from 'eslint';
2
- export declare const noRedundantImport: Rule.RuleModule;
@@ -1,114 +0,0 @@
1
- const SETTINGS_KEY = "skill-hub/autoImports";
2
- export const noRedundantImport = {
3
- meta: {
4
- type: "suggestion",
5
- docs: {
6
- description: "Disallow explicit imports of auto-imported identifiers in Nuxt"
7
- },
8
- fixable: "code",
9
- schema: [],
10
- messages: {
11
- redundant: "'{{ name }}' is auto-imported by Nuxt. Remove this explicit import."
12
- }
13
- },
14
- create(context) {
15
- const entries = context.settings[SETTINGS_KEY];
16
- if (!entries?.length)
17
- return {};
18
- const lookup = /* @__PURE__ */ new Map();
19
- for (const entry of entries) {
20
- const localName = entry.as || entry.name;
21
- const existing = lookup.get(entry.from) || { type: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() };
22
- addAutoImportName(existing[entry.type ? "type" : "value"], localName, entry.name);
23
- lookup.set(entry.from, existing);
24
- }
25
- return {
26
- ImportDeclaration(node) {
27
- const importNode = node;
28
- if (!importNode.specifiers.length) return;
29
- const source = typeof importNode.source.value === "string" ? importNode.source.value : void 0;
30
- if (!source) return;
31
- const autoImported = lookup.get(source);
32
- if (!autoImported) return;
33
- if (isDeclarationFile(context.filename)) return;
34
- const specInfos = [];
35
- for (const spec of importNode.specifiers) {
36
- if (spec.type !== "ImportSpecifier") {
37
- specInfos.push({ spec, localName: "", isRedundant: false });
38
- continue;
39
- }
40
- const localName = spec.local.name;
41
- const names = isTypeOnlyImport(importNode, spec) ? autoImported.type : autoImported.value;
42
- const importedName = getImportedName(spec);
43
- specInfos.push({ spec, localName, isRedundant: importedName ? names.get(localName)?.has(importedName) === true : false });
44
- }
45
- const redundant = specInfos.filter((s) => s.isRedundant);
46
- if (!redundant.length) return;
47
- const kept = specInfos.filter((s) => !s.isRedundant);
48
- const sourceText = context.sourceCode.getText();
49
- if (!kept.length) {
50
- context.report({
51
- node: importNode,
52
- messageId: "redundant",
53
- data: { name: redundant.map((s) => s.localName).join(", ") },
54
- fix: (fixer) => fixer.removeRange(importRemovalRange(sourceText, importNode.range))
55
- });
56
- return;
57
- }
58
- context.report({
59
- node: importNode,
60
- messageId: "redundant",
61
- data: { name: redundant.map((s) => s.localName).join(", ") },
62
- fix: (fixer) => fixer.replaceText(importNode, rebuildImportDeclaration(context, importNode, kept))
63
- });
64
- }
65
- };
66
- }
67
- };
68
- function isDeclarationFile(filename) {
69
- return /\.d\.(?:cts|mts|ts)$/.test(filename);
70
- }
71
- function importRemovalRange(text, range) {
72
- let end = range[1];
73
- while (text[end] === " " || text[end] === " ") {
74
- end++;
75
- }
76
- if (text.startsWith("\r\n", end)) {
77
- return [range[0], end + 2];
78
- }
79
- if (text[end] === "\n") {
80
- return [range[0], end + 1];
81
- }
82
- return [range[0], end];
83
- }
84
- function isTypeOnlyImport(node, spec) {
85
- return node.importKind === "type" || spec.importKind === "type";
86
- }
87
- function addAutoImportName(lookup, localName, importedName) {
88
- const importedNames = lookup.get(localName) || /* @__PURE__ */ new Set();
89
- importedNames.add(importedName);
90
- lookup.set(localName, importedNames);
91
- }
92
- function getImportedName(spec) {
93
- return typeof spec.imported.name === "string" ? spec.imported.name : typeof spec.imported.value === "string" ? spec.imported.value : void 0;
94
- }
95
- function rebuildImportDeclaration(context, node, kept) {
96
- const defaultSpecifiers = kept.filter(({ spec }) => spec.type === "ImportDefaultSpecifier");
97
- const namespaceSpecifiers = kept.filter(({ spec }) => spec.type === "ImportNamespaceSpecifier");
98
- const namedSpecifiers = kept.filter(({ spec }) => spec.type === "ImportSpecifier").map(({ spec }) => spec);
99
- const useTypeKeyword = node.importKind !== "type" && !defaultSpecifiers.length && !namespaceSpecifiers.length && namedSpecifiers.length > 0 && namedSpecifiers.every((spec) => spec.importKind === "type");
100
- const specifierTexts = [
101
- ...defaultSpecifiers.map(({ spec }) => context.sourceCode.getText(spec)),
102
- ...namespaceSpecifiers.map(({ spec }) => context.sourceCode.getText(spec))
103
- ];
104
- if (namedSpecifiers.length) {
105
- const namedTexts = namedSpecifiers.map((spec) => {
106
- const text = context.sourceCode.getText(spec);
107
- return useTypeKeyword ? text.replace(/^type\s+/, "") : text;
108
- });
109
- specifierTexts.push(`{ ${namedTexts.join(", ")} }`);
110
- }
111
- const sourceRange = node.source.range;
112
- const suffix = context.sourceCode.text.slice(sourceRange[1], node.range[1]);
113
- return `import${node.importKind === "type" || useTypeKeyword ? " type" : ""} ${specifierTexts.join(", ")} from ${context.sourceCode.getText(node.source)}${suffix}`;
114
- }
@@ -1,9 +0,0 @@
1
- declare const _default: {
2
- meta: {
3
- name: string;
4
- };
5
- rules: {
6
- 'no-redundant-import': import("eslint").Rule.RuleModule;
7
- };
8
- };
9
- export default _default;
@@ -1,5 +0,0 @@
1
- import { noRedundantImport } from "./no-redundant-import.js";
2
- export default {
3
- meta: { name: "skill-hub" },
4
- rules: { "no-redundant-import": noRedundantImport }
5
- };