nuxt-skill-hub 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +276 -66
- package/dist/runtime/eslint/no-redundant-import.d.ts +2 -0
- package/dist/runtime/eslint/no-redundant-import.js +114 -0
- package/dist/runtime/eslint/plugin.d.ts +9 -0
- package/dist/runtime/eslint/plugin.js +5 -0
- package/package.json +4 -1
package/dist/module.d.mts
CHANGED
|
@@ -11,6 +11,11 @@ interface ModuleOptions {
|
|
|
11
11
|
targets?: SkillHubTarget[];
|
|
12
12
|
moduleAuthoring?: boolean;
|
|
13
13
|
generationMode?: SkillHubGenerationMode;
|
|
14
|
+
/**
|
|
15
|
+
* Auto-register ESLint rule to remove redundant imports when @nuxt/eslint is installed.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
eslint?: boolean;
|
|
14
19
|
}
|
|
15
20
|
interface SkillHubContribution {
|
|
16
21
|
packageName: string;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useLogger, defineNuxtModule } from '@nuxt/kit';
|
|
2
|
-
import { isCI, isTest } from 'std-env';
|
|
2
|
+
import { isAgent, agent, isCI, isTest } from 'std-env';
|
|
3
3
|
import { promises, existsSync, lstatSync, writeFileSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { join, basename, resolve, dirname, relative, isAbsolute } from 'pathe';
|
|
5
5
|
import { fileURLToPath } from 'mlly';
|
|
@@ -16,7 +16,7 @@ import { glob } from 'tinyglobby';
|
|
|
16
16
|
import { createConsola } from 'consola';
|
|
17
17
|
import { colorize } from 'consola/utils';
|
|
18
18
|
|
|
19
|
-
const version = "0.0.
|
|
19
|
+
const version = "0.0.7";
|
|
20
20
|
const packageJson = {
|
|
21
21
|
version: version};
|
|
22
22
|
|
|
@@ -1062,6 +1062,9 @@ function findGitHubOverride(packageName) {
|
|
|
1062
1062
|
|
|
1063
1063
|
const GITHUB_API_BASE = "https://api.github.com";
|
|
1064
1064
|
const UNGH_API_BASE = "https://ungh.cc";
|
|
1065
|
+
const githubDirectoryCache = /* @__PURE__ */ new Map();
|
|
1066
|
+
const githubDefaultBranchCache = /* @__PURE__ */ new Map();
|
|
1067
|
+
const githubFileTextCache = /* @__PURE__ */ new Map();
|
|
1065
1068
|
function githubFetchOptions(timeoutMs) {
|
|
1066
1069
|
return {
|
|
1067
1070
|
timeout: timeoutMs,
|
|
@@ -1096,6 +1099,15 @@ function toRepoPath(value) {
|
|
|
1096
1099
|
}
|
|
1097
1100
|
return null;
|
|
1098
1101
|
}
|
|
1102
|
+
function memoize(cache, key, factory) {
|
|
1103
|
+
const cached = cache.get(key);
|
|
1104
|
+
if (cached) {
|
|
1105
|
+
return cached;
|
|
1106
|
+
}
|
|
1107
|
+
const pending = factory();
|
|
1108
|
+
cache.set(key, pending);
|
|
1109
|
+
return pending;
|
|
1110
|
+
}
|
|
1099
1111
|
function parseGitHubRepo(input) {
|
|
1100
1112
|
if (!input) {
|
|
1101
1113
|
return null;
|
|
@@ -1109,25 +1121,27 @@ async function listGitHubDirectory(repo, ref, dirPath, timeoutMs) {
|
|
|
1109
1121
|
}
|
|
1110
1122
|
const normalizedDirPath = dirPath.replace(/^\/+|\/+$/g, "");
|
|
1111
1123
|
const prefix = normalizedDirPath ? `${normalizedDirPath}/` : "";
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1124
|
+
return await memoize(githubDirectoryCache, `${repoPath}::${ref}::${normalizedDirPath}`, async () => {
|
|
1125
|
+
try {
|
|
1126
|
+
const data = await ofetch(
|
|
1127
|
+
`${GITHUB_API_BASE}/repos/${repoPath}/git/trees/${encodeURIComponent(ref)}?recursive=1`,
|
|
1128
|
+
githubFetchOptions(timeoutMs)
|
|
1129
|
+
);
|
|
1130
|
+
const entries = /* @__PURE__ */ new Set();
|
|
1131
|
+
for (const entry of data.tree || []) {
|
|
1132
|
+
if (entry.type !== "tree" || !entry.path.startsWith(prefix)) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const remainder = entry.path.slice(prefix.length);
|
|
1136
|
+
if (remainder && !remainder.includes("/")) {
|
|
1137
|
+
entries.add(remainder);
|
|
1138
|
+
}
|
|
1125
1139
|
}
|
|
1140
|
+
return Array.from(entries).sort((a, b) => a.localeCompare(b));
|
|
1141
|
+
} catch {
|
|
1142
|
+
return [];
|
|
1126
1143
|
}
|
|
1127
|
-
|
|
1128
|
-
} catch {
|
|
1129
|
-
return [];
|
|
1130
|
-
}
|
|
1144
|
+
});
|
|
1131
1145
|
}
|
|
1132
1146
|
async function fetchGitHubDefaultBranch(repo, timeoutMs) {
|
|
1133
1147
|
const repoPath = toRepoPath(repo);
|
|
@@ -1138,38 +1152,42 @@ async function fetchGitHubDefaultBranch(repo, timeoutMs) {
|
|
|
1138
1152
|
if (!owner || !name) {
|
|
1139
1153
|
return null;
|
|
1140
1154
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1155
|
+
return await memoize(githubDefaultBranchCache, repoPath, async () => {
|
|
1156
|
+
try {
|
|
1157
|
+
const data = await ofetch(
|
|
1158
|
+
`${UNGH_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
|
|
1159
|
+
githubFetchOptions(timeoutMs)
|
|
1160
|
+
);
|
|
1161
|
+
return data.repo?.defaultBranch || null;
|
|
1162
|
+
} catch {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1150
1166
|
}
|
|
1151
1167
|
async function fetchGitHubFileText(repo, ref, filePath, timeoutMs) {
|
|
1152
1168
|
const repoPath = toRepoPath(repo);
|
|
1153
1169
|
if (!repoPath) {
|
|
1154
1170
|
return { ok: false, error: "Invalid GitHub repository format" };
|
|
1155
1171
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1172
|
+
return await memoize(githubFileTextCache, `${repoPath}::${ref}::${filePath}`, async () => {
|
|
1173
|
+
try {
|
|
1174
|
+
const data = await ofetch(
|
|
1175
|
+
`https://raw.githubusercontent.com/${repoPath}/${encodeURIComponent(ref)}/${encodeGitHubPath(filePath)}`,
|
|
1176
|
+
{
|
|
1177
|
+
...githubFetchOptions(timeoutMs),
|
|
1178
|
+
responseType: "text"
|
|
1179
|
+
}
|
|
1180
|
+
);
|
|
1181
|
+
return { ok: true, data };
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
const status = typeof error === "object" && error && "status" in error ? Number(error.status) : void 0;
|
|
1184
|
+
return {
|
|
1185
|
+
ok: false,
|
|
1186
|
+
status,
|
|
1187
|
+
error: error.message
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1173
1191
|
}
|
|
1174
1192
|
|
|
1175
1193
|
function makeSkip(packageName, skillName, reason, sourceKind) {
|
|
@@ -1225,6 +1243,15 @@ function resolveRepositoryUrl(packageInfo) {
|
|
|
1225
1243
|
repoUrl: normalizeHttpUrl(packageInfo.repository)
|
|
1226
1244
|
};
|
|
1227
1245
|
}
|
|
1246
|
+
function isImmutableRef(ref, version) {
|
|
1247
|
+
if (!ref) {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
if (version && (ref === version || ref === `v${version}`)) {
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
return /^[a-f0-9]{7,40}$/i.test(ref);
|
|
1254
|
+
}
|
|
1228
1255
|
async function materializeCandidate(packageInfo, candidate, cacheRoot) {
|
|
1229
1256
|
const targetDir = join(
|
|
1230
1257
|
cacheRoot,
|
|
@@ -1232,8 +1259,24 @@ async function materializeCandidate(packageInfo, candidate, cacheRoot) {
|
|
|
1232
1259
|
sanitizeSegment(candidate.sourceRepo),
|
|
1233
1260
|
sanitizeSegment(candidate.sourceRef),
|
|
1234
1261
|
sanitizeSegment(packageInfo.packageName),
|
|
1262
|
+
sanitizeSegment(packageInfo.version || "unknown"),
|
|
1235
1263
|
sanitizeSegment(candidate.skillName)
|
|
1236
1264
|
);
|
|
1265
|
+
const skillFilePath = join(targetDir, "SKILL.md");
|
|
1266
|
+
if (isImmutableRef(candidate.sourceRef, packageInfo.version) && await pathExists(skillFilePath)) {
|
|
1267
|
+
return normalizeContribution({
|
|
1268
|
+
packageName: packageInfo.packageName,
|
|
1269
|
+
version: packageInfo.version,
|
|
1270
|
+
skillName: candidate.skillName,
|
|
1271
|
+
sourceKind: candidate.sourceKind,
|
|
1272
|
+
sourceRepo: candidate.sourceRepo,
|
|
1273
|
+
sourceRef: candidate.sourceRef,
|
|
1274
|
+
sourcePath: candidate.sourcePath,
|
|
1275
|
+
official: candidate.official,
|
|
1276
|
+
resolver: candidate.resolver,
|
|
1277
|
+
forceIncludeScripts: true
|
|
1278
|
+
}, targetDir, join(targetDir, ".."));
|
|
1279
|
+
}
|
|
1237
1280
|
try {
|
|
1238
1281
|
await downloadTemplate(`gh:${candidate.sourceRepo}/${candidate.sourcePath}#${candidate.sourceRef}`, {
|
|
1239
1282
|
dir: targetDir,
|
|
@@ -1245,7 +1288,7 @@ async function materializeCandidate(packageInfo, candidate, cacheRoot) {
|
|
|
1245
1288
|
} catch {
|
|
1246
1289
|
return null;
|
|
1247
1290
|
}
|
|
1248
|
-
if (!await pathExists(
|
|
1291
|
+
if (!await pathExists(skillFilePath)) {
|
|
1249
1292
|
return null;
|
|
1250
1293
|
}
|
|
1251
1294
|
return normalizeContribution({
|
|
@@ -1276,10 +1319,7 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
|
|
|
1276
1319
|
const refs = dedupe([
|
|
1277
1320
|
override?.ref || "",
|
|
1278
1321
|
packageInfo.version ? `v${packageInfo.version}` : "",
|
|
1279
|
-
packageInfo.version || ""
|
|
1280
|
-
await fetchGitHubDefaultBranch(repo, timeoutMs) || "",
|
|
1281
|
-
"main",
|
|
1282
|
-
"master"
|
|
1322
|
+
packageInfo.version || ""
|
|
1283
1323
|
]);
|
|
1284
1324
|
const candidates = dedupe([
|
|
1285
1325
|
override?.skillName || "",
|
|
@@ -1295,7 +1335,7 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
|
|
|
1295
1335
|
];
|
|
1296
1336
|
})
|
|
1297
1337
|
]);
|
|
1298
|
-
|
|
1338
|
+
const tryResolveForRef = async (ref, allowTreeLookup) => {
|
|
1299
1339
|
const packageJson = await fetchGitHubFileText(repo, ref, "package.json", timeoutMs);
|
|
1300
1340
|
if (packageJson.ok && packageJson.data) {
|
|
1301
1341
|
try {
|
|
@@ -1313,13 +1353,16 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
|
|
|
1313
1353
|
resolver: "agentsField"
|
|
1314
1354
|
}, cacheRoot);
|
|
1315
1355
|
if (resolved) {
|
|
1316
|
-
return
|
|
1356
|
+
return [resolved];
|
|
1317
1357
|
}
|
|
1318
1358
|
}
|
|
1319
1359
|
} catch {
|
|
1320
1360
|
issues.push(createValidationIssue(packageInfo.packageName, packageInfo.packageName, "Failed to parse remote package.json", "github"));
|
|
1321
1361
|
}
|
|
1322
1362
|
}
|
|
1363
|
+
if (!allowTreeLookup) {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1323
1366
|
const skillsDirs = await listGitHubDirectory(repo, ref, "skills", timeoutMs);
|
|
1324
1367
|
if (skillsDirs.length) {
|
|
1325
1368
|
const contributions = [];
|
|
@@ -1338,7 +1381,7 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
|
|
|
1338
1381
|
}
|
|
1339
1382
|
}
|
|
1340
1383
|
if (contributions.length) {
|
|
1341
|
-
return
|
|
1384
|
+
return contributions;
|
|
1342
1385
|
}
|
|
1343
1386
|
}
|
|
1344
1387
|
for (const path of pathCandidates) {
|
|
@@ -1353,9 +1396,33 @@ async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
|
|
|
1353
1396
|
resolver: "githubHeuristic"
|
|
1354
1397
|
}, cacheRoot);
|
|
1355
1398
|
if (resolved) {
|
|
1356
|
-
return
|
|
1399
|
+
return [resolved];
|
|
1357
1400
|
}
|
|
1358
1401
|
}
|
|
1402
|
+
return null;
|
|
1403
|
+
};
|
|
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
|
+
const fallbackRefs = dedupe([
|
|
1411
|
+
await fetchGitHubDefaultBranch(repo, timeoutMs) || "",
|
|
1412
|
+
"main",
|
|
1413
|
+
"master"
|
|
1414
|
+
]);
|
|
1415
|
+
for (const ref of fallbackRefs) {
|
|
1416
|
+
const resolved = await tryResolveForRef(ref, true);
|
|
1417
|
+
if (resolved?.length) {
|
|
1418
|
+
return { contributions: resolved, issues, skipped };
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
for (const ref of refs) {
|
|
1422
|
+
const resolved = await tryResolveForRef(ref, true);
|
|
1423
|
+
if (resolved?.length) {
|
|
1424
|
+
return { contributions: resolved, issues, skipped };
|
|
1425
|
+
}
|
|
1359
1426
|
}
|
|
1360
1427
|
skipped.push(makeSkip(packageInfo.packageName, override?.skillName || packageInfo.packageName, "No GitHub skill found using configured refs and path heuristics", "github"));
|
|
1361
1428
|
return {
|
|
@@ -1380,8 +1447,31 @@ async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
|
|
|
1380
1447
|
"generated",
|
|
1381
1448
|
"metadata-router",
|
|
1382
1449
|
sanitizeSegment(packageInfo.packageName),
|
|
1450
|
+
sanitizeSegment(packageInfo.version || "unknown"),
|
|
1383
1451
|
sanitizeSegment(skillName)
|
|
1384
1452
|
);
|
|
1453
|
+
const skillFilePath = join(targetDir, "SKILL.md");
|
|
1454
|
+
if (await pathExists(skillFilePath)) {
|
|
1455
|
+
return {
|
|
1456
|
+
contributions: [
|
|
1457
|
+
normalizeContribution({
|
|
1458
|
+
packageName: packageInfo.packageName,
|
|
1459
|
+
version: packageInfo.version,
|
|
1460
|
+
skillName,
|
|
1461
|
+
description: packageInfo.description,
|
|
1462
|
+
sourceKind: "generated",
|
|
1463
|
+
sourceRepo,
|
|
1464
|
+
repoUrl,
|
|
1465
|
+
docsUrl,
|
|
1466
|
+
official: true,
|
|
1467
|
+
resolver: "metadataRouter",
|
|
1468
|
+
forceIncludeScripts: false
|
|
1469
|
+
}, targetDir, join(targetDir, ".."))
|
|
1470
|
+
],
|
|
1471
|
+
issues: [],
|
|
1472
|
+
skipped: []
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1385
1475
|
const files = createMetadataRouterSkillFiles({
|
|
1386
1476
|
packageName: packageInfo.packageName,
|
|
1387
1477
|
skillName,
|
|
@@ -1613,6 +1703,7 @@ async function isGeneratedSkillFresh(generatedSkillRoot, fingerprint) {
|
|
|
1613
1703
|
}
|
|
1614
1704
|
|
|
1615
1705
|
const GITHUB_LOOKUP_TIMEOUT_MS = 1500;
|
|
1706
|
+
const REMOTE_RESOLUTION_CONCURRENCY = 4;
|
|
1616
1707
|
function resolveStableSkillBuildDir(rootDir, buildDir) {
|
|
1617
1708
|
const rel = relative(resolve(rootDir), resolve(buildDir)).replaceAll("\\", "/");
|
|
1618
1709
|
if (rel === ".nuxt" || rel.startsWith(".nuxt/")) {
|
|
@@ -1630,6 +1721,30 @@ function normalizeRelativePath(from, to) {
|
|
|
1630
1721
|
function getGeneratedSkillRoot(buildDir, skillName) {
|
|
1631
1722
|
return join(buildDir, "skill-hub", skillName);
|
|
1632
1723
|
}
|
|
1724
|
+
function getPersistentCacheRoot(exportRoot) {
|
|
1725
|
+
return join(exportRoot, "node_modules", ".cache", "nuxt-skill-hub");
|
|
1726
|
+
}
|
|
1727
|
+
function getCachedGeneratedSkillRoot(cacheRoot, skillName) {
|
|
1728
|
+
return join(cacheRoot, "generated", skillName);
|
|
1729
|
+
}
|
|
1730
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
1731
|
+
if (!items.length) {
|
|
1732
|
+
return [];
|
|
1733
|
+
}
|
|
1734
|
+
const results = [];
|
|
1735
|
+
let nextIndex = 0;
|
|
1736
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
1737
|
+
while (true) {
|
|
1738
|
+
const index = nextIndex++;
|
|
1739
|
+
if (index >= items.length) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
results[index] = await mapper(items[index], index);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
await Promise.all(workers);
|
|
1746
|
+
return results;
|
|
1747
|
+
}
|
|
1633
1748
|
function mergeSkipped(entries, keyFn) {
|
|
1634
1749
|
const byKey = /* @__PURE__ */ new Map();
|
|
1635
1750
|
for (const entry of entries) {
|
|
@@ -1706,6 +1821,8 @@ async function ensureStableSkillWrappers(input) {
|
|
|
1706
1821
|
async function generateSkillTree(input) {
|
|
1707
1822
|
const { nuxt, logger, options, skillName, exportRoot, generationMode } = input;
|
|
1708
1823
|
const stableBuildDir = resolveStableSkillBuildDir(nuxt.options.rootDir, nuxt.options.buildDir);
|
|
1824
|
+
const persistentCacheRoot = getPersistentCacheRoot(exportRoot);
|
|
1825
|
+
const cachedGeneratedSkillRoot = getCachedGeneratedSkillRoot(persistentCacheRoot, skillName);
|
|
1709
1826
|
const generatedSkillRoot = getGeneratedSkillRoot(stableBuildDir, skillName);
|
|
1710
1827
|
const referencesRoot = join(generatedSkillRoot, "references");
|
|
1711
1828
|
const nuxtRoot = join(referencesRoot, "nuxt");
|
|
@@ -1739,26 +1856,32 @@ async function generateSkillTree(input) {
|
|
|
1739
1856
|
...sortAndDedupeContributions(resolvedManual)
|
|
1740
1857
|
])
|
|
1741
1858
|
});
|
|
1742
|
-
if (generationMode === "prepare" && await isGeneratedSkillFresh(
|
|
1743
|
-
|
|
1859
|
+
if (generationMode === "prepare" && await isGeneratedSkillFresh(cachedGeneratedSkillRoot, fingerprint)) {
|
|
1860
|
+
await copySkillTree(cachedGeneratedSkillRoot, generatedSkillRoot, true);
|
|
1861
|
+
logger.info(`Restored cached ${skillName} skill from ${cachedGeneratedSkillRoot}`);
|
|
1744
1862
|
return;
|
|
1745
1863
|
}
|
|
1746
1864
|
const distResolvedPackages = new Set(discoveredContributions.contributions.map((item) => item.packageName));
|
|
1747
1865
|
const remoteIssues = [];
|
|
1748
1866
|
const remoteSkipped = [];
|
|
1749
1867
|
const remoteContributions = [];
|
|
1750
|
-
const remoteCacheRoot = join(
|
|
1751
|
-
await
|
|
1868
|
+
const remoteCacheRoot = join(persistentCacheRoot, "remote");
|
|
1869
|
+
await ensureDir(remoteCacheRoot);
|
|
1752
1870
|
const packagesToResolve = installedPackages.filter((pkg) => pkg.packageName !== "nuxt-skill-hub" && !distResolvedPackages.has(pkg.packageName));
|
|
1753
1871
|
const totalToResolve = packagesToResolve.length;
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1872
|
+
const remoteResults = await mapWithConcurrency(
|
|
1873
|
+
packagesToResolve,
|
|
1874
|
+
REMOTE_RESOLUTION_CONCURRENCY,
|
|
1875
|
+
async (pkg, index) => {
|
|
1876
|
+
logger.start(`Resolving skills for ${pkg.packageName} (${index + 1}/${totalToResolve})...`);
|
|
1877
|
+
return await resolveRemoteContributionsForPackage(pkg, {
|
|
1878
|
+
cacheRoot: remoteCacheRoot,
|
|
1879
|
+
githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
|
|
1880
|
+
enableGithubLookup: true
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
);
|
|
1884
|
+
for (const remote of remoteResults) {
|
|
1762
1885
|
remoteIssues.push(...remote.issues);
|
|
1763
1886
|
remoteSkipped.push(...remote.skipped);
|
|
1764
1887
|
remoteContributions.push(...remote.contributions);
|
|
@@ -1841,14 +1964,56 @@ async function generateSkillTree(input) {
|
|
|
1841
1964
|
skipped
|
|
1842
1965
|
));
|
|
1843
1966
|
await writeGenerationState(generatedSkillRoot, fingerprint);
|
|
1967
|
+
await copySkillTree(generatedSkillRoot, cachedGeneratedSkillRoot, true);
|
|
1844
1968
|
logger.success(`Generated ${skillName} skill at ${generatedSkillRoot}`);
|
|
1845
1969
|
}
|
|
1846
1970
|
|
|
1847
1971
|
function isCancelled(value) {
|
|
1848
1972
|
return value === null || typeof value === "symbol";
|
|
1849
1973
|
}
|
|
1974
|
+
const AI_AGENT_TARGET_MAP = {
|
|
1975
|
+
codex: "codex",
|
|
1976
|
+
claude: "claude-code",
|
|
1977
|
+
gemini: "gemini-code-assist",
|
|
1978
|
+
replit: "replit-agent"
|
|
1979
|
+
};
|
|
1980
|
+
function resolveAIGuidanceTarget(currentTarget, detectedAgent) {
|
|
1981
|
+
if (currentTarget) {
|
|
1982
|
+
return currentTarget;
|
|
1983
|
+
}
|
|
1984
|
+
if (!detectedAgent) {
|
|
1985
|
+
return void 0;
|
|
1986
|
+
}
|
|
1987
|
+
return AI_AGENT_TARGET_MAP[detectedAgent];
|
|
1988
|
+
}
|
|
1989
|
+
function buildAIGuidance(currentTarget, detectedAgent) {
|
|
1990
|
+
const target = resolveAIGuidanceTarget(currentTarget, detectedAgent);
|
|
1991
|
+
const sourceLabel = detectedAgent ? ` (${detectedAgent})` : "";
|
|
1992
|
+
const snippetLines = [
|
|
1993
|
+
"export default defineNuxtConfig({",
|
|
1994
|
+
" skillHub: {",
|
|
1995
|
+
...target ? [` targets: ['${target}'],`] : [],
|
|
1996
|
+
` generationMode: 'prepare',`,
|
|
1997
|
+
" },",
|
|
1998
|
+
"})"
|
|
1999
|
+
];
|
|
2000
|
+
return {
|
|
2001
|
+
intro: `Install wizard skipped because nuxt-skill-hub was installed by an AI agent${sourceLabel}. Add this to your nuxt.config.ts:`,
|
|
2002
|
+
snippet: snippetLines.join("\n"),
|
|
2003
|
+
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
|
+
};
|
|
2005
|
+
}
|
|
1850
2006
|
async function runInstallWizard(nuxt) {
|
|
1851
2007
|
const logger = useLogger("nuxt-skill-hub");
|
|
2008
|
+
if (isAgent) {
|
|
2009
|
+
const guidance = buildAIGuidance(detectCurrentTarget(), agent);
|
|
2010
|
+
logger.info(guidance.intro);
|
|
2011
|
+
logger.info(`\`\`\`ts
|
|
2012
|
+
${guidance.snippet}
|
|
2013
|
+
\`\`\``);
|
|
2014
|
+
logger.info(guidance.optionalFlags);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
1852
2017
|
if (process.env.CI || !process.stdout.isTTY) {
|
|
1853
2018
|
logger.info("Non-interactive environment detected, skipping install wizard.");
|
|
1854
2019
|
return;
|
|
@@ -2005,6 +2170,49 @@ Configure \`skillHub.skillName\`, \`skillHub.targets\`, or set \`skillHub.genera
|
|
|
2005
2170
|
);
|
|
2006
2171
|
}
|
|
2007
2172
|
|
|
2173
|
+
function createAutoImportAddon(nuxt) {
|
|
2174
|
+
let unimport;
|
|
2175
|
+
let nitroUnimport;
|
|
2176
|
+
const hook = nuxt.hook;
|
|
2177
|
+
hook("imports:context", (context) => {
|
|
2178
|
+
unimport = context;
|
|
2179
|
+
});
|
|
2180
|
+
hook("nitro:init", (nitro) => {
|
|
2181
|
+
nitroUnimport = nitro.unimport;
|
|
2182
|
+
});
|
|
2183
|
+
hook("eslint:config:addons", (addons) => {
|
|
2184
|
+
addons.push({
|
|
2185
|
+
name: "skill-hub:no-redundant-import",
|
|
2186
|
+
async getConfigs() {
|
|
2187
|
+
const imports = [
|
|
2188
|
+
...await unimport?.getImports() || [],
|
|
2189
|
+
...await nitroUnimport?.getImports() || []
|
|
2190
|
+
];
|
|
2191
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2192
|
+
const entries = imports.filter((i) => {
|
|
2193
|
+
const key = `${i.as || i.name}:${i.from}:${i.type ? "type" : "value"}`;
|
|
2194
|
+
if (seen.has(key)) return false;
|
|
2195
|
+
seen.add(key);
|
|
2196
|
+
return true;
|
|
2197
|
+
}).map((i) => ({ name: i.name, ...i.as && i.as !== i.name ? { as: i.as } : {}, from: i.from, ...i.type ? { type: true } : {} })).sort((a, b) => a.from.localeCompare(b.from) || a.name.localeCompare(b.name));
|
|
2198
|
+
return {
|
|
2199
|
+
imports: [{ from: "nuxt-skill-hub/eslint-plugin", name: "default", as: "skillHubPlugin" }],
|
|
2200
|
+
configs: [
|
|
2201
|
+
[
|
|
2202
|
+
"{",
|
|
2203
|
+
` name: 'skill-hub/no-redundant-import',`,
|
|
2204
|
+
` plugins: { 'skill-hub': skillHubPlugin },`,
|
|
2205
|
+
` settings: { 'skill-hub/autoImports': ${JSON.stringify(entries)} },`,
|
|
2206
|
+
` rules: { 'skill-hub/no-redundant-import': 'warn' },`,
|
|
2207
|
+
"}"
|
|
2208
|
+
].join("\n")
|
|
2209
|
+
]
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2008
2216
|
const module$1 = defineNuxtModule({
|
|
2009
2217
|
meta: {
|
|
2010
2218
|
name: "nuxt-skill-hub",
|
|
@@ -2025,6 +2233,8 @@ const module$1 = defineNuxtModule({
|
|
|
2025
2233
|
},
|
|
2026
2234
|
async setup(options, nuxt) {
|
|
2027
2235
|
const logger = useLogger("nuxt-skill-hub");
|
|
2236
|
+
if (options.eslint !== false)
|
|
2237
|
+
createAutoImportAddon(nuxt);
|
|
2028
2238
|
if (isCI && !isTest) {
|
|
2029
2239
|
logger.info("Skipping skill generation in CI");
|
|
2030
2240
|
return;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const SETTINGS_KEY = "skill-hub/autoImports";
|
|
2
|
+
export const noRedundantImport = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "suggestion",
|
|
5
|
+
docs: {
|
|
6
|
+
description: "Disallow explicit imports of auto-imported identifiers in Nuxt"
|
|
7
|
+
},
|
|
8
|
+
fixable: "code",
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
redundant: "'{{ name }}' is auto-imported by Nuxt. Remove this explicit import."
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
const entries = context.settings[SETTINGS_KEY];
|
|
16
|
+
if (!entries?.length)
|
|
17
|
+
return {};
|
|
18
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const localName = entry.as || entry.name;
|
|
21
|
+
const existing = lookup.get(entry.from) || { type: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() };
|
|
22
|
+
addAutoImportName(existing[entry.type ? "type" : "value"], localName, entry.name);
|
|
23
|
+
lookup.set(entry.from, existing);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
ImportDeclaration(node) {
|
|
27
|
+
const importNode = node;
|
|
28
|
+
if (!importNode.specifiers.length) return;
|
|
29
|
+
const source = typeof importNode.source.value === "string" ? importNode.source.value : void 0;
|
|
30
|
+
if (!source) return;
|
|
31
|
+
const autoImported = lookup.get(source);
|
|
32
|
+
if (!autoImported) return;
|
|
33
|
+
if (isDeclarationFile(context.filename)) return;
|
|
34
|
+
const specInfos = [];
|
|
35
|
+
for (const spec of importNode.specifiers) {
|
|
36
|
+
if (spec.type !== "ImportSpecifier") {
|
|
37
|
+
specInfos.push({ spec, localName: "", isRedundant: false });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const localName = spec.local.name;
|
|
41
|
+
const names = isTypeOnlyImport(importNode, spec) ? autoImported.type : autoImported.value;
|
|
42
|
+
const importedName = getImportedName(spec);
|
|
43
|
+
specInfos.push({ spec, localName, isRedundant: importedName ? names.get(localName)?.has(importedName) === true : false });
|
|
44
|
+
}
|
|
45
|
+
const redundant = specInfos.filter((s) => s.isRedundant);
|
|
46
|
+
if (!redundant.length) return;
|
|
47
|
+
const kept = specInfos.filter((s) => !s.isRedundant);
|
|
48
|
+
const sourceText = context.sourceCode.getText();
|
|
49
|
+
if (!kept.length) {
|
|
50
|
+
context.report({
|
|
51
|
+
node: importNode,
|
|
52
|
+
messageId: "redundant",
|
|
53
|
+
data: { name: redundant.map((s) => s.localName).join(", ") },
|
|
54
|
+
fix: (fixer) => fixer.removeRange(importRemovalRange(sourceText, importNode.range))
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
context.report({
|
|
59
|
+
node: importNode,
|
|
60
|
+
messageId: "redundant",
|
|
61
|
+
data: { name: redundant.map((s) => s.localName).join(", ") },
|
|
62
|
+
fix: (fixer) => fixer.replaceText(importNode, rebuildImportDeclaration(context, importNode, kept))
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function isDeclarationFile(filename) {
|
|
69
|
+
return /\.d\.(?:cts|mts|ts)$/.test(filename);
|
|
70
|
+
}
|
|
71
|
+
function importRemovalRange(text, range) {
|
|
72
|
+
let end = range[1];
|
|
73
|
+
while (text[end] === " " || text[end] === " ") {
|
|
74
|
+
end++;
|
|
75
|
+
}
|
|
76
|
+
if (text.startsWith("\r\n", end)) {
|
|
77
|
+
return [range[0], end + 2];
|
|
78
|
+
}
|
|
79
|
+
if (text[end] === "\n") {
|
|
80
|
+
return [range[0], end + 1];
|
|
81
|
+
}
|
|
82
|
+
return [range[0], end];
|
|
83
|
+
}
|
|
84
|
+
function isTypeOnlyImport(node, spec) {
|
|
85
|
+
return node.importKind === "type" || spec.importKind === "type";
|
|
86
|
+
}
|
|
87
|
+
function addAutoImportName(lookup, localName, importedName) {
|
|
88
|
+
const importedNames = lookup.get(localName) || /* @__PURE__ */ new Set();
|
|
89
|
+
importedNames.add(importedName);
|
|
90
|
+
lookup.set(localName, importedNames);
|
|
91
|
+
}
|
|
92
|
+
function getImportedName(spec) {
|
|
93
|
+
return typeof spec.imported.name === "string" ? spec.imported.name : typeof spec.imported.value === "string" ? spec.imported.value : void 0;
|
|
94
|
+
}
|
|
95
|
+
function rebuildImportDeclaration(context, node, kept) {
|
|
96
|
+
const defaultSpecifiers = kept.filter(({ spec }) => spec.type === "ImportDefaultSpecifier");
|
|
97
|
+
const namespaceSpecifiers = kept.filter(({ spec }) => spec.type === "ImportNamespaceSpecifier");
|
|
98
|
+
const namedSpecifiers = kept.filter(({ spec }) => spec.type === "ImportSpecifier").map(({ spec }) => spec);
|
|
99
|
+
const useTypeKeyword = node.importKind !== "type" && !defaultSpecifiers.length && !namespaceSpecifiers.length && namedSpecifiers.length > 0 && namedSpecifiers.every((spec) => spec.importKind === "type");
|
|
100
|
+
const specifierTexts = [
|
|
101
|
+
...defaultSpecifiers.map(({ spec }) => context.sourceCode.getText(spec)),
|
|
102
|
+
...namespaceSpecifiers.map(({ spec }) => context.sourceCode.getText(spec))
|
|
103
|
+
];
|
|
104
|
+
if (namedSpecifiers.length) {
|
|
105
|
+
const namedTexts = namedSpecifiers.map((spec) => {
|
|
106
|
+
const text = context.sourceCode.getText(spec);
|
|
107
|
+
return useTypeKeyword ? text.replace(/^type\s+/, "") : text;
|
|
108
|
+
});
|
|
109
|
+
specifierTexts.push(`{ ${namedTexts.join(", ")} }`);
|
|
110
|
+
}
|
|
111
|
+
const sourceRange = node.source.range;
|
|
112
|
+
const suffix = context.sourceCode.text.slice(sourceRange[1], node.range[1]);
|
|
113
|
+
return `import${node.importKind === "type" || useTypeKeyword ? " type" : ""} ${specifierTexts.join(", ")} from ${context.sourceCode.getText(node.source)}${suffix}`;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-skill-hub",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Teach your AI agent the Nuxt way with best practices and module guidance.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"types": "./dist/types.d.mts",
|
|
14
14
|
"import": "./dist/module.mjs",
|
|
15
15
|
"default": "./dist/module.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./eslint-plugin": {
|
|
18
|
+
"import": "./dist/runtime/eslint/plugin.js"
|
|
16
19
|
}
|
|
17
20
|
},
|
|
18
21
|
"main": "./dist/module.mjs",
|