nuxt-skill-hub 0.0.5 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-skill-hub",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "configKey": "skillHub",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.3.0"
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.5";
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
- try {
1113
- const data = await ofetch(
1114
- `${GITHUB_API_BASE}/repos/${repoPath}/git/trees/${encodeURIComponent(ref)}?recursive=1`,
1115
- githubFetchOptions(timeoutMs)
1116
- );
1117
- const entries = /* @__PURE__ */ new Set();
1118
- for (const entry of data.tree || []) {
1119
- if (entry.type !== "tree" || !entry.path.startsWith(prefix)) {
1120
- continue;
1121
- }
1122
- const remainder = entry.path.slice(prefix.length);
1123
- if (remainder && !remainder.includes("/")) {
1124
- entries.add(remainder);
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
- return Array.from(entries).sort((a, b) => a.localeCompare(b));
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
- try {
1142
- const data = await ofetch(
1143
- `${UNGH_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
1144
- githubFetchOptions(timeoutMs)
1145
- );
1146
- return data.repo?.defaultBranch || null;
1147
- } catch {
1148
- return null;
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
- try {
1157
- const data = await ofetch(
1158
- `https://raw.githubusercontent.com/${repoPath}/${encodeURIComponent(ref)}/${encodeGitHubPath(filePath)}`,
1159
- {
1160
- ...githubFetchOptions(timeoutMs),
1161
- responseType: "text"
1162
- }
1163
- );
1164
- return { ok: true, data };
1165
- } catch (error) {
1166
- const status = typeof error === "object" && error && "status" in error ? Number(error.status) : void 0;
1167
- return {
1168
- ok: false,
1169
- status,
1170
- error: error.message
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(join(targetDir, "SKILL.md"))) {
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
- for (const ref of refs) {
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 { contributions: [resolved], issues, skipped };
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 { contributions, issues, skipped };
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 { contributions: [resolved], issues, skipped };
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(generatedSkillRoot, fingerprint)) {
1743
- logger.info(`Generated ${skillName} skill is already up to date at ${generatedSkillRoot}`);
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(stableBuildDir, "skill-hub-cache");
1751
- await emptyDir(remoteCacheRoot);
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
- for (let i = 0; i < packagesToResolve.length; i++) {
1755
- const pkg = packagesToResolve[i];
1756
- logger.start(`[${i + 1}/${totalToResolve}] Resolving skills for ${pkg.packageName}...`);
1757
- const remote = await resolveRemoteContributionsForPackage(pkg, {
1758
- cacheRoot: remoteCacheRoot,
1759
- githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
1760
- enableGithubLookup: true
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,10 +2233,16 @@ 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;
2031
2241
  }
2242
+ if (process.argv.slice(2).includes("typecheck")) {
2243
+ logger.info("Skipping skill generation during typecheck");
2244
+ return;
2245
+ }
2032
2246
  const configuredSkillName = options.skillName?.trim();
2033
2247
  let resolvedSkillName;
2034
2248
  if (configuredSkillName && isValidSkillName(configuredSkillName)) {
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const noRedundantImport: Rule.RuleModule;
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ meta: {
3
+ name: string;
4
+ };
5
+ rules: {
6
+ 'no-redundant-import': import("eslint").Rule.RuleModule;
7
+ };
8
+ };
9
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import { noRedundantImport } from "./no-redundant-import.js";
2
+ export default {
3
+ meta: { name: "skill-hub" },
4
+ rules: { "no-redundant-import": noRedundantImport }
5
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-skill-hub",
3
- "version": "0.0.5",
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",