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