nuxt-skill-hub 0.0.8 → 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/README.md +11 -1
- package/dist/module.d.mts +42 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +509 -70
- package/package.json +23 -24
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
|
|
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,14 +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;
|
|
51
|
+
remote?: boolean | SkillHubRemoteOptions;
|
|
14
52
|
/**
|
|
15
53
|
* Auto-register ESLint rule to remove redundant imports when @nuxt/eslint is installed.
|
|
16
54
|
* @default true
|
|
@@ -42,6 +80,9 @@ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, fa
|
|
|
42
80
|
declare module '@nuxt/schema' {
|
|
43
81
|
interface NuxtHooks {
|
|
44
82
|
'skill-hub:contribute': (ctx: SkillHubContributionContext) => void | Promise<void>;
|
|
83
|
+
'skill-hub:resolve:start': (ctx: SkillHubResolveStartContext) => void | Promise<void>;
|
|
84
|
+
'skill-hub:resolve:package': (ctx: SkillHubResolvePackageContext) => void | Promise<void>;
|
|
85
|
+
'skill-hub:resolve:done': (ctx: SkillHubResolveDoneContext) => void | Promise<void>;
|
|
45
86
|
}
|
|
46
87
|
}
|
|
47
88
|
|
package/dist/module.json
CHANGED
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
|
|
21
|
+
const version = "0.1.0";
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
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
|
|
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
|
|
2931
|
+
selectedTargets = normalizeTargetSelection(chosen);
|
|
2493
2932
|
}
|
|
2494
2933
|
}
|
|
2495
2934
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-skill-hub",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Teach your AI agent the Nuxt way with best practices and module guidance.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,37 +24,36 @@
|
|
|
24
24
|
"nuxt-best-practices"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@nuxt/kit": "^4.
|
|
27
|
+
"@nuxt/kit": "^4.4.6",
|
|
28
28
|
"automd": "^0.4.3",
|
|
29
|
-
"consola": "^3.4.
|
|
30
|
-
"giget": "^3.
|
|
29
|
+
"consola": "^3.4.2",
|
|
30
|
+
"giget": "^3.2.0",
|
|
31
31
|
"gray-matter": "^4.0.3",
|
|
32
|
-
"mlly": "^1.8.
|
|
33
|
-
"ofetch": "^1.5.
|
|
32
|
+
"mlly": "^1.8.2",
|
|
33
|
+
"ofetch": "^1.5.1",
|
|
34
34
|
"ohash": "^2.0.11",
|
|
35
|
-
"pathe": "^2.0.
|
|
36
|
-
"pkg-types": "^2.3.
|
|
37
|
-
"std-env": "^4.
|
|
38
|
-
"tinyglobby": "^0.2.
|
|
35
|
+
"pathe": "^2.0.3",
|
|
36
|
+
"pkg-types": "^2.3.1",
|
|
37
|
+
"std-env": "^4.1.0",
|
|
38
|
+
"tinyglobby": "^0.2.17",
|
|
39
39
|
"unagent": "^0.0.8"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@nuxt/devtools": "^3.2.
|
|
43
|
-
"@nuxt/eslint-config": "^1.15.
|
|
42
|
+
"@nuxt/devtools": "^3.2.4",
|
|
43
|
+
"@nuxt/eslint-config": "^1.15.2",
|
|
44
44
|
"@nuxt/module-builder": "^1.0.2",
|
|
45
|
-
"@nuxt/schema": "^4.
|
|
46
|
-
"@nuxt/test-utils": "^4.0.
|
|
47
|
-
"@
|
|
48
|
-
"@
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"nuxt": "^4.3.1",
|
|
45
|
+
"@nuxt/schema": "^4.4.6",
|
|
46
|
+
"@nuxt/test-utils": "^4.0.3",
|
|
47
|
+
"@types/node": "^25.9.1",
|
|
48
|
+
"@vue/test-utils": "^2.4.10",
|
|
49
|
+
"bumpp": "^11.1.0",
|
|
50
|
+
"eslint": "^10.4.1",
|
|
51
|
+
"happy-dom": "^20.9.0",
|
|
52
|
+
"nuxt": "^4.4.6",
|
|
54
53
|
"typescript": "~5.9.3",
|
|
55
|
-
"vitest": "^4.
|
|
56
|
-
"vue": "^3.5.
|
|
57
|
-
"vue-tsc": "^3.
|
|
54
|
+
"vitest": "^4.1.7",
|
|
55
|
+
"vue": "^3.5.35",
|
|
56
|
+
"vue-tsc": "^3.3.3"
|
|
58
57
|
},
|
|
59
58
|
"scripts": {
|
|
60
59
|
"dev": "pnpm --dir docs dev",
|