nuxt-skill-hub 0.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/module.d.mts +41 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +1912 -0
- package/dist/types.d.mts +3 -0
- package/nuxt-best-practices/index.template.md +74 -0
- package/nuxt-best-practices/rules/abstraction-disambiguation.md +58 -0
- package/nuxt-best-practices/rules/architecture-boundaries.md +58 -0
- package/nuxt-best-practices/rules/content-modeling-navigation.md +58 -0
- package/nuxt-best-practices/rules/data-fetching-ssr.md +115 -0
- package/nuxt-best-practices/rules/error-surfaces-recovery.md +39 -0
- package/nuxt-best-practices/rules/hydration-consistency.md +58 -0
- package/nuxt-best-practices/rules/migrations.md +58 -0
- package/nuxt-best-practices/rules/module-authoring.md +77 -0
- package/nuxt-best-practices/rules/nitro-h3-patterns.md +96 -0
- package/nuxt-best-practices/rules/nuxt-ui-primitives.md +58 -0
- package/nuxt-best-practices/rules/page-meta-head-layout.md +58 -0
- package/nuxt-best-practices/rules/performance-rendering.md +77 -0
- package/nuxt-best-practices/rules/plugins.md +58 -0
- package/nuxt-best-practices/rules/server-routes-runtime-config.md +58 -0
- package/nuxt-best-practices/rules/verification-finish.md +58 -0
- package/package.json +64 -0
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,1912 @@
|
|
|
1
|
+
import { promises, existsSync, lstatSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, basename, resolve, dirname, relative, isAbsolute } from 'pathe';
|
|
3
|
+
import { useLogger, defineNuxtModule } from '@nuxt/kit';
|
|
4
|
+
import { resolvePackageJSON, readPackageJSON } from 'pkg-types';
|
|
5
|
+
import { createConsola } from 'consola';
|
|
6
|
+
import { detectInstalledAgents, expandPath, detectCurrentAgent, getAgentIds, getAgentConfig } from 'unagent/env';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import matter from 'gray-matter';
|
|
9
|
+
import { transform } from 'automd';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { downloadTemplate } from 'giget';
|
|
12
|
+
import { ofetch } from 'ofetch';
|
|
13
|
+
|
|
14
|
+
const version = "0.0.1-beta.1";
|
|
15
|
+
const packageJson = {
|
|
16
|
+
version: version};
|
|
17
|
+
|
|
18
|
+
const metadata = packageJson;
|
|
19
|
+
const PACKAGE_VERSION = metadata.version;
|
|
20
|
+
|
|
21
|
+
function resolveSkillsDir(target, config) {
|
|
22
|
+
if (config?.skillsDir) {
|
|
23
|
+
return config.skillsDir;
|
|
24
|
+
}
|
|
25
|
+
if (target === "codex") {
|
|
26
|
+
return "skills";
|
|
27
|
+
}
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
function normalizeTarget(target) {
|
|
31
|
+
return target.trim();
|
|
32
|
+
}
|
|
33
|
+
function getRawTargetConfig(target) {
|
|
34
|
+
return getAgentConfig(target);
|
|
35
|
+
}
|
|
36
|
+
function getSupportedTargets() {
|
|
37
|
+
return getAgentIds().filter((target) => {
|
|
38
|
+
const config = getRawTargetConfig(target);
|
|
39
|
+
return Boolean(resolveSkillsDir(target, config));
|
|
40
|
+
}).sort((a, b) => a.localeCompare(b));
|
|
41
|
+
}
|
|
42
|
+
function resolveAgentTargetConfig(target) {
|
|
43
|
+
const normalized = normalizeTarget(target);
|
|
44
|
+
if (!normalized) {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
const config = getRawTargetConfig(normalized);
|
|
48
|
+
const skillsDir = resolveSkillsDir(normalized, config);
|
|
49
|
+
if (!config || !skillsDir) {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
target: normalized,
|
|
54
|
+
configDir: expandPath(config.configDir),
|
|
55
|
+
skillsDir
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function validateTargets(targets) {
|
|
59
|
+
const uniqueTargets = Array.from(new Set(targets.map(normalizeTarget).filter(Boolean)));
|
|
60
|
+
const valid = [];
|
|
61
|
+
const invalid = [];
|
|
62
|
+
for (const target of uniqueTargets) {
|
|
63
|
+
const config = getRawTargetConfig(target);
|
|
64
|
+
if (!config) {
|
|
65
|
+
invalid.push({
|
|
66
|
+
target,
|
|
67
|
+
reason: "unknown-target"
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!resolveSkillsDir(target, config)) {
|
|
72
|
+
invalid.push({
|
|
73
|
+
target,
|
|
74
|
+
reason: "missing-skills-dir"
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
valid.push(target);
|
|
79
|
+
}
|
|
80
|
+
return { valid, invalid };
|
|
81
|
+
}
|
|
82
|
+
function detectCurrentTarget() {
|
|
83
|
+
const current = detectCurrentAgent();
|
|
84
|
+
if (!current) return void 0;
|
|
85
|
+
const skillsDir = resolveSkillsDir(current.id, current.config);
|
|
86
|
+
return skillsDir ? current.id : void 0;
|
|
87
|
+
}
|
|
88
|
+
function detectInstalledTargets(rootDir) {
|
|
89
|
+
return Array.from(new Set(
|
|
90
|
+
detectInstalledAgents().filter((agent) => agent.detected === "config" && resolveSkillsDir(agent.id, agent.config)).map((agent) => agent.id)
|
|
91
|
+
)).sort((a, b) => a.localeCompare(b));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const DEFAULT_NUXT_CONTENT_METADATA = {
|
|
95
|
+
id: "nuxt-best-practices",
|
|
96
|
+
version: PACKAGE_VERSION,
|
|
97
|
+
experimental: true,
|
|
98
|
+
description: "Nuxt-native decision packs and best-practice rule packs for AI execution. Complements docs, not a replacement.",
|
|
99
|
+
packIds: [
|
|
100
|
+
"abstraction-disambiguation",
|
|
101
|
+
"page-meta-head-layout",
|
|
102
|
+
"error-surfaces-recovery",
|
|
103
|
+
"verification-finish",
|
|
104
|
+
"data-fetching-ssr",
|
|
105
|
+
"hydration-consistency",
|
|
106
|
+
"architecture-boundaries",
|
|
107
|
+
"server-routes-runtime-config",
|
|
108
|
+
"nitro-h3-patterns",
|
|
109
|
+
"plugins",
|
|
110
|
+
"performance-rendering",
|
|
111
|
+
"module-authoring",
|
|
112
|
+
"migrations"
|
|
113
|
+
],
|
|
114
|
+
packs: [
|
|
115
|
+
{
|
|
116
|
+
id: "abstraction-disambiguation",
|
|
117
|
+
title: "Abstraction Disambiguation",
|
|
118
|
+
focus: "Choose Nuxt-owned abstractions before generic Vue, raw HTML, or ad hoc glue code.",
|
|
119
|
+
triggers: ["A generic Vue or HTML fix looks plausible", "You need to decide whether Nuxt, Nuxt Content, or Nuxt UI already owns the surface"],
|
|
120
|
+
relativePath: "rules/abstraction-disambiguation.md"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "page-meta-head-layout",
|
|
124
|
+
title: "Page Meta, Head, and Layout",
|
|
125
|
+
focus: "Separate page behavior, document metadata, and layout structure before editing.",
|
|
126
|
+
triggers: ["Pages, layouts, middleware, or page options", "Title, meta tags, canonical URLs, or OG metadata"],
|
|
127
|
+
relativePath: "rules/page-meta-head-layout.md"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "error-surfaces-recovery",
|
|
131
|
+
title: "Error Surfaces and Recovery",
|
|
132
|
+
focus: "Cover both global and local error boundaries and use the right recovery utilities.",
|
|
133
|
+
triggers: ["Global error pages or local error boundaries", "Recovery flows, clearError, showError, or fallback UI"],
|
|
134
|
+
relativePath: "rules/error-surfaces-recovery.md"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "verification-finish",
|
|
138
|
+
title: "Verification and Finish",
|
|
139
|
+
focus: "Verify the intended framework behavior and re-check paired surfaces before concluding work.",
|
|
140
|
+
triggers: ["The fix spans more than one surface", "You are about to finish a Nuxt change and need to confirm the right abstraction"],
|
|
141
|
+
relativePath: "rules/verification-finish.md"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "data-fetching-ssr",
|
|
145
|
+
title: "Data Fetching and SSR",
|
|
146
|
+
focus: "Deduplication, payload correctness, and request-safe loading.",
|
|
147
|
+
triggers: ["Pages, layouts, or composables fetching initial data", "SSR-visible content or hydration-sensitive state"],
|
|
148
|
+
relativePath: "rules/data-fetching-ssr.md"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "hydration-consistency",
|
|
152
|
+
title: "Hydration and SSR Consistency",
|
|
153
|
+
focus: "SSR/CSR determinism, client-only boundaries, and mismatch prevention.",
|
|
154
|
+
triggers: ["Hydration warnings or client-only rendering", "State that differs between server and browser"],
|
|
155
|
+
relativePath: "rules/hydration-consistency.md"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "architecture-boundaries",
|
|
159
|
+
title: "Architecture Boundaries",
|
|
160
|
+
focus: "Server-only secrets, request isolation, and safe shared abstractions.",
|
|
161
|
+
triggers: ["Crossing server/client boundaries", "Composable or utility architecture changes"],
|
|
162
|
+
relativePath: "rules/architecture-boundaries.md"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "server-routes-runtime-config",
|
|
166
|
+
title: "Server Routes and Runtime Config",
|
|
167
|
+
focus: "Runtime config exposure, env handling, and route-level contracts.",
|
|
168
|
+
triggers: ["nuxt.config, runtime config, or env wiring", "Server route config and public/private config exposure"],
|
|
169
|
+
relativePath: "rules/server-routes-runtime-config.md"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "nitro-h3-patterns",
|
|
173
|
+
title: "Nitro and h3 Server Patterns",
|
|
174
|
+
focus: "Handler contracts, caching, and edge-safe server behavior.",
|
|
175
|
+
triggers: ["Server routes, middleware, Nitro plugins, or h3 handlers", "API behavior, caching, or edge runtime concerns"],
|
|
176
|
+
relativePath: "rules/nitro-h3-patterns.md"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "plugins",
|
|
180
|
+
title: "Plugins and Runtime Boot",
|
|
181
|
+
focus: "Plugin ordering, startup cost, and app/runtime initialization.",
|
|
182
|
+
triggers: ["Plugins, app boot logic, or injected runtime helpers", "Global runtime behavior changes"],
|
|
183
|
+
relativePath: "rules/plugins.md"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "performance-rendering",
|
|
187
|
+
title: "Performance and Rendering",
|
|
188
|
+
focus: "Rendering strategy, payload cost, and lazy loading tradeoffs.",
|
|
189
|
+
triggers: ["Performance regressions or rendering strategy changes", "Link, asset, and bundle cost decisions"],
|
|
190
|
+
relativePath: "rules/performance-rendering.md"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "module-authoring",
|
|
194
|
+
title: "Module Authoring Conventions",
|
|
195
|
+
focus: "Nuxt Kit patterns, module lifecycle, and ecosystem-safe authoring.",
|
|
196
|
+
triggers: ["Writing or refactoring a Nuxt module", "Module install/setup conventions"],
|
|
197
|
+
relativePath: "rules/module-authoring.md"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "migrations",
|
|
201
|
+
title: "Migrations and Compatibility",
|
|
202
|
+
focus: "Incremental upgrades, compatibility boundaries, and rollout safety.",
|
|
203
|
+
triggers: ["Upgrades, deprecations, or breaking behavior changes", "Compatibility fixes across Nuxt versions"],
|
|
204
|
+
relativePath: "rules/migrations.md"
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
};
|
|
208
|
+
const EMPTY_MODULE_GUIDANCE_MARKDOWN = "_No module skills discovered. Use Nuxt guidance plus official module docs when module-specific guidance is missing._";
|
|
209
|
+
function yamlString(value) {
|
|
210
|
+
return JSON.stringify(value);
|
|
211
|
+
}
|
|
212
|
+
function getSourceLabel(sourceKind) {
|
|
213
|
+
switch (sourceKind) {
|
|
214
|
+
case "dist":
|
|
215
|
+
return "Installed package skill";
|
|
216
|
+
case "github":
|
|
217
|
+
return "Resolved module skill";
|
|
218
|
+
case "generated":
|
|
219
|
+
return "Metadata-routed skill";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function getTrustLevel(official) {
|
|
223
|
+
return official ? "official" : "community";
|
|
224
|
+
}
|
|
225
|
+
function formatCompactLinks(entry, labels) {
|
|
226
|
+
const links = [];
|
|
227
|
+
if (entry.docsUrl) {
|
|
228
|
+
links.push(`${labels.docs}: [${entry.docsUrl}](${entry.docsUrl})`);
|
|
229
|
+
}
|
|
230
|
+
if (entry.repoUrl) {
|
|
231
|
+
links.push(`${labels.repo}: [${entry.repoUrl}](${entry.repoUrl})`);
|
|
232
|
+
}
|
|
233
|
+
return links.join("\n");
|
|
234
|
+
}
|
|
235
|
+
function resolveMetadataRouterSkillName(packageName) {
|
|
236
|
+
if (packageName.startsWith("@")) {
|
|
237
|
+
const [scope, name] = packageName.split("/");
|
|
238
|
+
if (scope === "@nuxt" && name) {
|
|
239
|
+
return `nuxt-${name}`;
|
|
240
|
+
}
|
|
241
|
+
return (name || scope?.replace(/^@/, "") || "").trim();
|
|
242
|
+
}
|
|
243
|
+
return packageName.trim();
|
|
244
|
+
}
|
|
245
|
+
function createMetadataRouterSkillFiles(input) {
|
|
246
|
+
const description = input.description?.trim() || `Metadata-routed module skill for ${input.packageName}. Use it to reach the official docs and upstream source.`;
|
|
247
|
+
const linkLines = createCompactMetadataRouterContent(input).trim();
|
|
248
|
+
return {
|
|
249
|
+
"SKILL.md": `---
|
|
250
|
+
name: ${yamlString(input.skillName)}
|
|
251
|
+
description: ${yamlString(description)}
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
${linkLines || "Docs: unavailable\nSource code: unavailable"}
|
|
255
|
+
`
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function withDerivedModuleFields(entry) {
|
|
259
|
+
return {
|
|
260
|
+
...entry,
|
|
261
|
+
sourceLabel: entry.sourceLabel || getSourceLabel(entry.sourceKind),
|
|
262
|
+
trustLevel: entry.trustLevel || getTrustLevel(entry.official)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function formatVersion(version) {
|
|
266
|
+
return version ? `v${version}` : void 0;
|
|
267
|
+
}
|
|
268
|
+
function relativeModuleLink(entry, prefix) {
|
|
269
|
+
const path = entry.entryPath || entry.wrapperPath;
|
|
270
|
+
if (!path) {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
return `${prefix}${path.replace(/^references\/modules\//, "")}`;
|
|
274
|
+
}
|
|
275
|
+
function groupModuleEntries(entries) {
|
|
276
|
+
const officialUpstream = entries.filter((entry) => entry.sourceKind === "dist");
|
|
277
|
+
const githubResolved = entries.filter((entry) => entry.sourceKind === "github");
|
|
278
|
+
const metadataRouted = entries.filter((entry) => entry.sourceKind === "generated");
|
|
279
|
+
return { officialUpstream, githubResolved, metadataRouted };
|
|
280
|
+
}
|
|
281
|
+
function hasModuleGuidance(entries, skipped = []) {
|
|
282
|
+
return entries.length > 0 || skipped.length > 0;
|
|
283
|
+
}
|
|
284
|
+
function renderModuleGroup(title, entries, prefix) {
|
|
285
|
+
if (!entries.length) {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
const lines = entries.map((rawEntry) => {
|
|
289
|
+
const entry = withDerivedModuleFields(rawEntry);
|
|
290
|
+
const wrapperLink = relativeModuleLink(entry, prefix);
|
|
291
|
+
const description = entry.description ? ` ${entry.description}` : "";
|
|
292
|
+
const version = formatVersion(entry.version);
|
|
293
|
+
const versionLabel = version ? ` \`${version}\`` : "";
|
|
294
|
+
return `- [${entry.packageName}](${wrapperLink})${versionLabel} - ${entry.sourceLabel}. Trust: \`${entry.trustLevel}\`.${description}`;
|
|
295
|
+
});
|
|
296
|
+
return `### ${title}
|
|
297
|
+
${lines.join("\n")}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
function renderSkippedEntries(skipped) {
|
|
301
|
+
if (!skipped.length) {
|
|
302
|
+
return "";
|
|
303
|
+
}
|
|
304
|
+
const lines = skipped.map((entry) => {
|
|
305
|
+
const base = `- **${entry.packageName}** / \`${entry.skillName}\``;
|
|
306
|
+
const source = entry.sourceKind ? ` (\`${entry.sourceKind}\`)` : "";
|
|
307
|
+
return `${base}${source}: ${entry.reason}. Use the relevant Nuxt pack plus the module's official docs.`;
|
|
308
|
+
});
|
|
309
|
+
return `### Skipped or unavailable
|
|
310
|
+
${lines.join("\n")}
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
function findPack(metadata, id) {
|
|
314
|
+
return metadata.packs.find((pack) => pack.id === id) || metadata.packs[0];
|
|
315
|
+
}
|
|
316
|
+
function createPackSummary(metadata, packIds) {
|
|
317
|
+
return packIds.map((packId) => `\`${findPack(metadata, packId).title}\``).join(", ");
|
|
318
|
+
}
|
|
319
|
+
function getSkillRenderProfile(includeModuleAuthoring = false) {
|
|
320
|
+
if (includeModuleAuthoring) {
|
|
321
|
+
return {
|
|
322
|
+
includeModuleAuthoring,
|
|
323
|
+
packSummaryIds: [
|
|
324
|
+
"module-authoring",
|
|
325
|
+
"plugins",
|
|
326
|
+
"architecture-boundaries",
|
|
327
|
+
"nitro-h3-patterns",
|
|
328
|
+
"migrations"
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
includeModuleAuthoring: false,
|
|
334
|
+
packSummaryIds: [
|
|
335
|
+
"abstraction-disambiguation",
|
|
336
|
+
"page-meta-head-layout",
|
|
337
|
+
"error-surfaces-recovery",
|
|
338
|
+
"verification-finish",
|
|
339
|
+
"data-fetching-ssr"
|
|
340
|
+
]
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function packLink(metadata, id) {
|
|
344
|
+
const pack = findPack(metadata, id);
|
|
345
|
+
return `[${pack.title}](./references/nuxt/${pack.relativePath})`;
|
|
346
|
+
}
|
|
347
|
+
function createRoutingTable(metadata, includeModuleAuthoring = false) {
|
|
348
|
+
const rows = [
|
|
349
|
+
...includeModuleAuthoring ? [{
|
|
350
|
+
symptom: "Writing, refactoring, or publishing a Nuxt module",
|
|
351
|
+
packId: "module-authoring",
|
|
352
|
+
why: "Start with Nuxt Kit-safe authoring patterns, lifecycle hooks, prefixed public APIs, and skill scope boundaries."
|
|
353
|
+
}] : [],
|
|
354
|
+
{
|
|
355
|
+
symptom: "SSR, initial page load, route params, or hydration-sensitive data",
|
|
356
|
+
packId: "data-fetching-ssr",
|
|
357
|
+
why: "Prefer `useFetch` or `useAsyncData` over setup-time `$fetch` or `onMounted` fetches."
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
symptom: "Page options, route middleware, layout selection, title, meta tags, or OG data",
|
|
361
|
+
packId: "page-meta-head-layout",
|
|
362
|
+
why: "Separate page behavior from document metadata and layout structure before editing."
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
symptom: "A generic Vue fix or raw HTML implementation looks tempting",
|
|
366
|
+
packId: "abstraction-disambiguation",
|
|
367
|
+
why: "Check whether Nuxt, Nuxt Content, or Nuxt UI already owns the abstraction."
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
symptom: "The remaining work is mostly `.vue` components, composables, reactivity, or SFC structure",
|
|
371
|
+
link: "[Vue Best Practices](./references/vue/SKILL.md)",
|
|
372
|
+
why: "Switch to Vue-specific guidance after the Nuxt ownership and routing decisions are already settled."
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
symptom: "Global errors, local fallback UI, `clearError`, `showError`, or recovery flows",
|
|
376
|
+
packId: "error-surfaces-recovery",
|
|
377
|
+
why: "Nuxt error handling often needs both global and local surfaces to be correct."
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
symptom: "Secrets, runtime config, privileged API calls, or server/client boundary confusion",
|
|
381
|
+
packId: "architecture-boundaries",
|
|
382
|
+
why: "Move sensitive logic server-side first, then pair with config and route rules as needed."
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
symptom: "Before finishing a fix that spans multiple files or surfaces",
|
|
386
|
+
packId: "verification-finish",
|
|
387
|
+
why: "Re-check paired surfaces and verify framework behavior, not only the visible output."
|
|
388
|
+
}
|
|
389
|
+
];
|
|
390
|
+
return [
|
|
391
|
+
"| Task shape or symptom | Load first | Why |",
|
|
392
|
+
"| --- | --- | --- |",
|
|
393
|
+
...rows.map((row) => `| ${row.symptom} | ${"link" in row ? row.link : packLink(metadata, row.packId)} | ${row.why} |`)
|
|
394
|
+
].join("\n");
|
|
395
|
+
}
|
|
396
|
+
function createNuxtPackTable(metadata) {
|
|
397
|
+
const rows = metadata.packs.map(
|
|
398
|
+
(pack) => `| [${pack.title}](./references/nuxt/${pack.relativePath}) | ${pack.focus} | ${pack.triggers.join("<br>")} |`
|
|
399
|
+
);
|
|
400
|
+
return [
|
|
401
|
+
"| Pack | Focus | Typical triggers |",
|
|
402
|
+
"| --- | --- | --- |",
|
|
403
|
+
...rows
|
|
404
|
+
].join("\n");
|
|
405
|
+
}
|
|
406
|
+
function createVueGuidanceSection() {
|
|
407
|
+
return `## Vue guidance
|
|
408
|
+
|
|
409
|
+
Use [Vue Best Practices](./references/vue/SKILL.md) when the task is mainly Vue component, composable, reactivity, props/emits, or SFC work and Nuxt no longer owns the abstraction.
|
|
410
|
+
|
|
411
|
+
Start with these must-read Vue references:
|
|
412
|
+
- [reactivity](./references/vue/references/reactivity.md)
|
|
413
|
+
- [sfc](./references/vue/references/sfc.md)
|
|
414
|
+
- [component-data-flow](./references/vue/references/component-data-flow.md)
|
|
415
|
+
- [composables](./references/vue/references/composables.md)
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
function createSkillMapSections(metadata, entries, skipped = [], includeModuleAuthoring = false) {
|
|
419
|
+
const includeModuleSections = hasModuleGuidance(entries, skipped);
|
|
420
|
+
const grouped = groupModuleEntries(entries);
|
|
421
|
+
const moduleSections = [
|
|
422
|
+
renderModuleGroup("Official upstream skills", grouped.officialUpstream, "./references/modules/"),
|
|
423
|
+
renderModuleGroup("Resolved module skills", grouped.githubResolved, "./references/modules/"),
|
|
424
|
+
renderModuleGroup("Metadata-routed skills", grouped.metadataRouted, "./references/modules/"),
|
|
425
|
+
renderSkippedEntries(skipped)
|
|
426
|
+
].filter(Boolean);
|
|
427
|
+
const moduleGuideContent = moduleSections.join("\n");
|
|
428
|
+
const audienceGuide = includeModuleAuthoring ? `
|
|
429
|
+
## Module author focus
|
|
430
|
+
|
|
431
|
+
This skill includes extra guidance for Nuxt module authors on top of the default app-oriented Nuxt packs.
|
|
432
|
+
If the task touches \`defineNuxtModule\`, Nuxt Kit hooks, generated runtime files, prefixed public APIs, or release compatibility, start with ${packLink(metadata, "module-authoring")} before branching into the broader app-oriented packs.
|
|
433
|
+
` : "";
|
|
434
|
+
const moduleWorkflowSteps = includeModuleSections ? `
|
|
435
|
+
5. If an installed module is involved, open its entry under \`references/modules\`.
|
|
436
|
+
6. Copied skills go straight to their \`SKILL.md\`; metadata-routed modules only expose docs and source links.` : "";
|
|
437
|
+
const moduleGuidesSection = includeModuleSections ? `
|
|
438
|
+
## Module guides
|
|
439
|
+
|
|
440
|
+
Open the linked module entry first. Copied skills link straight to their \`SKILL.md\`; metadata-routed modules use a small docs/source router when no copied skill exists.
|
|
441
|
+
|
|
442
|
+
${moduleGuideContent}
|
|
443
|
+
` : `
|
|
444
|
+
## Module guides
|
|
445
|
+
|
|
446
|
+
${EMPTY_MODULE_GUIDANCE_MARKDOWN}
|
|
447
|
+
`;
|
|
448
|
+
return `## How to use this skill map
|
|
449
|
+
1. Explore the current surface first: page, layout, component, server handler, content collection, or module-owned file.
|
|
450
|
+
2. Load the first matching Nuxt pack from the routing table below.
|
|
451
|
+
3. If the remaining work is mainly Vue component or composable implementation, switch to [Vue Best Practices](./references/vue/SKILL.md).
|
|
452
|
+
4. Open deeper Nuxt packs only when the first pack points you there.
|
|
453
|
+
${moduleWorkflowSteps}
|
|
454
|
+
${audienceGuide}
|
|
455
|
+
|
|
456
|
+
## Common forks in the road
|
|
457
|
+
|
|
458
|
+
${createRoutingTable(metadata, includeModuleAuthoring)}
|
|
459
|
+
|
|
460
|
+
${createVueGuidanceSection()}
|
|
461
|
+
|
|
462
|
+
## All Nuxt packs
|
|
463
|
+
|
|
464
|
+
${createNuxtPackTable(metadata)}
|
|
465
|
+
${moduleGuidesSection}
|
|
466
|
+
`;
|
|
467
|
+
}
|
|
468
|
+
function createSkillEntrypoint(skillName, metadata, monorepoScopePath, includeModuleAuthoring = false, entries = [], skipped = []) {
|
|
469
|
+
const profile = getSkillRenderProfile(includeModuleAuthoring);
|
|
470
|
+
const includeModuleSections = hasModuleGuidance(entries, skipped);
|
|
471
|
+
const description = "Always-on Nuxt disambiguation layer for this project. Use it to choose the right Nuxt pack first, then switch to Vue or module guidance only when Nuxt no longer owns the abstraction.";
|
|
472
|
+
const monorepoScopeSection = monorepoScopePath ? `
|
|
473
|
+
## Monorepo Scope
|
|
474
|
+
This skill applies only to the \`${monorepoScopePath}\` subtree of this monorepo.
|
|
475
|
+
Treat files and tasks outside \`${monorepoScopePath}\` as out of scope unless the user explicitly redirects you there.
|
|
476
|
+
` : "";
|
|
477
|
+
const audienceSection = profile.includeModuleAuthoring ? `
|
|
478
|
+
|
|
479
|
+
## Module Author Focus
|
|
480
|
+
This skill keeps the default Nuxt app guidance and adds an authoring layer for repositories that build Nuxt modules.
|
|
481
|
+
Start with [Module Authoring Conventions](./references/nuxt/rules/module-authoring.md) for \`defineNuxtModule\`, lifecycle hooks, prefixed public APIs, and module-scoped skill boundaries, then fall back to the broader app packs when the task crosses into runtime behavior.
|
|
482
|
+
` : "";
|
|
483
|
+
const activationFlow = `1. Explore the project first: inspect the real page, component, route, server handler, collection, or module surface you are changing.
|
|
484
|
+
2. Use the routing sections in this file and load the smallest matching Nuxt pack.
|
|
485
|
+
3. If the remaining work is mainly Vue component, composable, reactivity, or SFC work, open [Vue Best Practices](./references/vue/SKILL.md).
|
|
486
|
+
4. If module authoring is part of the task, load [Module Authoring Conventions](./references/nuxt/rules/module-authoring.md) before changing \`defineNuxtModule\`, runtime extensions, hooks, or release scaffolding.${includeModuleSections ? `
|
|
487
|
+
5. If an installed module owns the problem, open its entry under [references/modules](./references/modules).
|
|
488
|
+
6. Apply module guidance as delta-only rules inside that module's APIs, config, runtime behavior, and owned files.` : ""}`;
|
|
489
|
+
const frequencyBullets = profile.includeModuleAuthoring ? `- If the task changes module boot or heavy setup work, prefer lightweight \`setup\` plus lifecycle hooks before adding blocking async work.
|
|
490
|
+
- If the module exposes routes, composables, components, or config, prefix public surfaces with module identity before shipping generic names.
|
|
491
|
+
- If the implementation relies on undocumented Nuxt internals, confirm there is not a public \`@nuxt/kit\` API or documented hook first.
|
|
492
|
+
- If the task adds or edits a bundled module skill, keep it strictly scoped to the module's APIs, integration points, and caveats.
|
|
493
|
+
- If runtime config, server handlers, or generated files are involved, pair module-author guidance with the relevant server, Nitro, plugin, or migration pack instead of improvising cross-boundary behavior.
|
|
494
|
+
- If the work is version-sensitive, check compatibility constraints and migration boundaries before expanding the module surface.
|
|
495
|
+
- If the task touches SSR, initial page load, or route-driven data, prefer \`useFetch\` or \`useAsyncData\` before \`onMounted\` plus \`$fetch\`.
|
|
496
|
+
- If the task changes page options, layout selection, route middleware, or page-level behavior, check \`definePageMeta\` before adding ad hoc wiring.
|
|
497
|
+
- If the task changes title, meta tags, canonical URLs, or OG data, check \`useHead\` or \`useSeoMeta\` before page-meta or template markup.
|
|
498
|
+
- If content lives in JSON or YAML records, or the UI needs generated docs navigation, choose data collections and collection-navigation primitives before manual assembly.
|
|
499
|
+
- If the UI surface is page chrome, a table, a form, a modal, a command palette, or a dropdown, prefer a Nuxt UI primitive before raw HTML or custom listeners.
|
|
500
|
+
- If runtime config, tokens, secrets, or privileged API calls are involved, keep them server-side and expose only a server route or the minimum public config.
|
|
501
|
+
- If hydration, browser-only APIs, time, randomness, or cookies are involved, use SSR-safe primitives first and isolate browser-only work behind \`ClientOnly\` or \`onMounted\`.
|
|
502
|
+
- If the fix touches errors, fallback UI, or recovery flow, check both global and local surfaces before concluding the work is complete.
|
|
503
|
+
- If the solution looks correct but uses generic Vue or hand-rolled HTML, confirm Nuxt, Nuxt Content, or Nuxt UI does not already own that abstraction.${includeModuleSections ? `
|
|
504
|
+
- If the task is module-specific, use the module entry and keep module guidance scoped; do not replace broad Nuxt rules with module-specific rules.` : ""}` : `- If the task touches SSR, initial page load, or route-driven data, prefer \`useFetch\` or \`useAsyncData\` before \`onMounted\` plus \`$fetch\`.
|
|
505
|
+
- If the task changes page options, layout selection, route middleware, or page-level behavior, check \`definePageMeta\` before adding ad hoc wiring.
|
|
506
|
+
- If the task changes title, meta tags, canonical URLs, or OG data, check \`useHead\` or \`useSeoMeta\` before page-meta or template markup.
|
|
507
|
+
- If content lives in JSON or YAML records, or the UI needs generated docs navigation, choose data collections and collection-navigation primitives before manual assembly.
|
|
508
|
+
- If the UI surface is page chrome, a table, a form, a modal, a command palette, or a dropdown, prefer a Nuxt UI primitive before raw HTML or custom listeners.
|
|
509
|
+
- If runtime config, tokens, secrets, or privileged API calls are involved, keep them server-side and expose only a server route or the minimum public config.
|
|
510
|
+
- If hydration, browser-only APIs, time, randomness, or cookies are involved, use SSR-safe primitives first and isolate browser-only work behind \`ClientOnly\` or \`onMounted\`.
|
|
511
|
+
- If the fix touches errors, fallback UI, or recovery flow, check both global and local surfaces before concluding the work is complete.
|
|
512
|
+
- If the solution looks correct but uses generic Vue or hand-rolled HTML, confirm Nuxt, Nuxt Content, or Nuxt UI does not already own that abstraction.${includeModuleSections ? `
|
|
513
|
+
- If the task is module-specific, use the module entry and keep module guidance scoped; do not replace broad Nuxt rules with module-specific rules.` : ""}`;
|
|
514
|
+
const beforeCompletion = profile.includeModuleAuthoring ? `- Did I start from Nuxt Kit and documented lifecycle hooks before reaching for private internals?
|
|
515
|
+
- Did I keep public APIs collision-resistant and module-scoped?
|
|
516
|
+
- Did I keep module skill guidance as a delta on top of Nuxt guidance instead of restating framework-global rules?
|
|
517
|
+
- Did I verify compatibility, install/setup cost, and cross-boundary behavior before concluding the work is complete?
|
|
518
|
+
- Did I still verify the relevant app/runtime surfaces when the module work crossed into SSR, Nitro, plugins, config, or UI behavior?` : `- Did I choose a Nuxt primitive where a generic Vue or raw HTML solution would be tempting?
|
|
519
|
+
- Did I check whether the fix needs a second surface such as global or local, or server or client?
|
|
520
|
+
- Did I choose the right concept pair: page meta vs head, data collection vs page collection, component primitive vs custom markup?
|
|
521
|
+
- Did I verify the framework behavior that matters, not just the visible output?`;
|
|
522
|
+
return `---
|
|
523
|
+
name: ${yamlString(skillName)}
|
|
524
|
+
description: ${yamlString(description)}
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
# Nuxt Skill Index
|
|
528
|
+
|
|
529
|
+
This file keeps the highest-frequency Nuxt decisions in context.
|
|
530
|
+
Use it to avoid generic Vue fallbacks, then route into the right Nuxt pack, Vue guidance, or module delta skill.
|
|
531
|
+
${monorepoScopeSection}
|
|
532
|
+
${audienceSection}
|
|
533
|
+
|
|
534
|
+
## Activation Flow
|
|
535
|
+
${activationFlow}
|
|
536
|
+
|
|
537
|
+
## High-Frequency Nuxt Decisions
|
|
538
|
+
${frequencyBullets}
|
|
539
|
+
|
|
540
|
+
## Precedence
|
|
541
|
+
- Repository-global instructions and required workflows win first.
|
|
542
|
+
- This file keeps the common Nuxt forks in context.
|
|
543
|
+
- Nuxt packs provide deeper framework guidance.
|
|
544
|
+
- Vue guidance covers component, composable, and SFC patterns after the Nuxt decision is settled.
|
|
545
|
+
${includeModuleSections ? "- Module entries add delta-only guidance inside explicit module scope." : ""}
|
|
546
|
+
|
|
547
|
+
${createSkillMapSections(metadata, entries, skipped, includeModuleAuthoring)}
|
|
548
|
+
|
|
549
|
+
## Before Completion
|
|
550
|
+
${beforeCompletion}
|
|
551
|
+
|
|
552
|
+
## Primary Packs
|
|
553
|
+
Start with ${createPackSummary(metadata, profile.packSummaryIds)} when the task matches one of those common routes.
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
_Generated by nuxt-skill-hub. Do not edit this file manually._
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
function createCompactMetadataRouterContent(entry) {
|
|
561
|
+
return `${formatCompactLinks(entry, {
|
|
562
|
+
docs: "Docs",
|
|
563
|
+
repo: "Source code"
|
|
564
|
+
})}
|
|
565
|
+
`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function loadNuxtRuleFilesFromDir(nuxtContentDir) {
|
|
569
|
+
const entries = [];
|
|
570
|
+
for await (const relativePath of promises.glob("**/*", { cwd: nuxtContentDir })) {
|
|
571
|
+
const absolutePath = join(nuxtContentDir, relativePath);
|
|
572
|
+
if (relativePath === "index.template.md" || relativePath.split("/").some((segment) => segment.startsWith(".")) || basename(relativePath).startsWith("_") || !(await promises.lstat(absolutePath)).isFile()) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
entries.push([relativePath, await promises.readFile(absolutePath, "utf8")]);
|
|
576
|
+
}
|
|
577
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
578
|
+
return Object.fromEntries(entries);
|
|
579
|
+
}
|
|
580
|
+
async function loadNuxtIndexTemplateFromDir(nuxtContentDir) {
|
|
581
|
+
return await promises.readFile(join(nuxtContentDir, "index.template.md"), "utf8");
|
|
582
|
+
}
|
|
583
|
+
async function loadNuxtMetadata$1() {
|
|
584
|
+
return DEFAULT_NUXT_CONTENT_METADATA;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const NUXT_CONTENT_SOURCE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../nuxt-best-practices");
|
|
588
|
+
async function loadNuxtRuleFiles() {
|
|
589
|
+
return await loadNuxtRuleFilesFromDir(NUXT_CONTENT_SOURCE_DIR);
|
|
590
|
+
}
|
|
591
|
+
async function loadNuxtIndexTemplate() {
|
|
592
|
+
return await loadNuxtIndexTemplateFromDir(NUXT_CONTENT_SOURCE_DIR);
|
|
593
|
+
}
|
|
594
|
+
async function loadNuxtMetadata() {
|
|
595
|
+
return await loadNuxtMetadata$1();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function loadVueSkillFilesFromDir(vueContentDir) {
|
|
599
|
+
const entries = [];
|
|
600
|
+
for await (const relativePath of promises.glob("**/*", { cwd: vueContentDir })) {
|
|
601
|
+
const absolutePath = join(vueContentDir, relativePath);
|
|
602
|
+
if (relativePath.split("/").some((segment) => segment.startsWith(".")) || basename(relativePath).startsWith("_") || !(await promises.lstat(absolutePath)).isFile()) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
entries.push([relativePath, await promises.readFile(absolutePath, "utf8")]);
|
|
606
|
+
}
|
|
607
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
608
|
+
return Object.fromEntries(entries);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const VUE_SKILLS_REPO = "vuejs-ai/skills";
|
|
612
|
+
const VUE_SKILLS_PATH = "skills/vue-best-practices";
|
|
613
|
+
const FALLBACK_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../vue-content");
|
|
614
|
+
async function resolveVueContentDir(cacheRoot) {
|
|
615
|
+
const targetDir = resolve(cacheRoot, "vue-best-practices");
|
|
616
|
+
try {
|
|
617
|
+
await downloadTemplate(`gh:${VUE_SKILLS_REPO}/${VUE_SKILLS_PATH}`, {
|
|
618
|
+
dir: targetDir,
|
|
619
|
+
force: true,
|
|
620
|
+
forceClean: true,
|
|
621
|
+
registry: false,
|
|
622
|
+
silent: true
|
|
623
|
+
});
|
|
624
|
+
return targetDir;
|
|
625
|
+
} catch {
|
|
626
|
+
if (existsSync(FALLBACK_DIR)) return FALLBACK_DIR;
|
|
627
|
+
throw new Error("Failed to fetch Vue skill content and no local fallback found");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
async function loadVueSkillFiles(cacheRoot) {
|
|
631
|
+
const dir = await resolveVueContentDir(cacheRoot);
|
|
632
|
+
return loadVueSkillFilesFromDir(dir);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const MANAGED_HINT_START = "<!-- nuxt-skill-hub:start -->";
|
|
636
|
+
const MANAGED_HINT_END = "<!-- nuxt-skill-hub:end -->";
|
|
637
|
+
const SKILL_NAME_MAX_LENGTH = 64;
|
|
638
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
639
|
+
async function pathExists(path) {
|
|
640
|
+
try {
|
|
641
|
+
await promises.access(path);
|
|
642
|
+
return true;
|
|
643
|
+
} catch {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async function ensureDir(path) {
|
|
648
|
+
await promises.mkdir(path, { recursive: true });
|
|
649
|
+
}
|
|
650
|
+
async function emptyDir(path) {
|
|
651
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
652
|
+
try {
|
|
653
|
+
await promises.rm(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
654
|
+
break;
|
|
655
|
+
} catch (error) {
|
|
656
|
+
const code = error.code;
|
|
657
|
+
if ((code === "ENOTEMPTY" || code === "EBUSY") && attempt < 3) {
|
|
658
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
await ensureDir(path);
|
|
665
|
+
}
|
|
666
|
+
async function writeFileIfChanged(path, contents) {
|
|
667
|
+
const previous = await pathExists(path) ? await promises.readFile(path, "utf8") : null;
|
|
668
|
+
if (previous === contents) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
await ensureDir(dirname(path));
|
|
672
|
+
await promises.writeFile(path, contents, "utf8");
|
|
673
|
+
}
|
|
674
|
+
async function hasWorkspacePackageConfig(path) {
|
|
675
|
+
const packageJsonPath = join(path, "package.json");
|
|
676
|
+
if (!await pathExists(packageJsonPath)) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const packageJson = JSON.parse(await promises.readFile(packageJsonPath, "utf8"));
|
|
681
|
+
return Array.isArray(packageJson.workspaces) || !!packageJson.workspaces && typeof packageJson.workspaces === "object";
|
|
682
|
+
} catch {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function resolveExportRoot(rootDir) {
|
|
687
|
+
const appRoot = resolve(rootDir);
|
|
688
|
+
let current = appRoot;
|
|
689
|
+
while (true) {
|
|
690
|
+
if (await pathExists(join(current, "pnpm-workspace.yaml")) || await hasWorkspacePackageConfig(current)) {
|
|
691
|
+
return current;
|
|
692
|
+
}
|
|
693
|
+
const parent = dirname(current);
|
|
694
|
+
if (parent === current) {
|
|
695
|
+
return appRoot;
|
|
696
|
+
}
|
|
697
|
+
current = parent;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function resolveMonorepoScopePath(rootDir, exportRoot) {
|
|
701
|
+
const appRoot = resolve(rootDir);
|
|
702
|
+
const resolvedExportRoot = resolve(exportRoot);
|
|
703
|
+
if (appRoot === resolvedExportRoot) {
|
|
704
|
+
return void 0;
|
|
705
|
+
}
|
|
706
|
+
const scopePath = relative(resolvedExportRoot, appRoot).replace(/\/+$/, "");
|
|
707
|
+
return scopePath || void 0;
|
|
708
|
+
}
|
|
709
|
+
function createValidationIssue(packageName, skillName, reason, sourceKind) {
|
|
710
|
+
return {
|
|
711
|
+
severity: "warning",
|
|
712
|
+
packageName,
|
|
713
|
+
skillName,
|
|
714
|
+
reason,
|
|
715
|
+
sourceKind
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function isValidSkillName(name) {
|
|
719
|
+
if (!name || name.length > SKILL_NAME_MAX_LENGTH) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
if (!SKILL_NAME_PATTERN.test(name)) {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
if (name.startsWith("-") || name.endsWith("-") || name.includes("--")) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
function parseAgentSkillDeclarations(pkg, packageName, sourceKind) {
|
|
731
|
+
const raw = pkg && typeof pkg === "object" ? pkg.agents?.skills : void 0;
|
|
732
|
+
if (!Array.isArray(raw)) {
|
|
733
|
+
return { skills: [], issues: [] };
|
|
734
|
+
}
|
|
735
|
+
const skills = [];
|
|
736
|
+
const issues = [];
|
|
737
|
+
for (const [index, entry] of raw.entries()) {
|
|
738
|
+
if (!entry || typeof entry !== "object") {
|
|
739
|
+
issues.push(createValidationIssue(packageName, `entry-${index + 1}`, "agents.skills entry must be an object", sourceKind));
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
|
743
|
+
const path = typeof entry.path === "string" ? entry.path.trim() : "";
|
|
744
|
+
if (!name) {
|
|
745
|
+
issues.push(createValidationIssue(packageName, `entry-${index + 1}`, 'agents.skills entry is missing a non-empty "name"', sourceKind));
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (!path) {
|
|
749
|
+
issues.push(createValidationIssue(packageName, name, 'agents.skills entry is missing a non-empty "path"', sourceKind));
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (!isValidSkillName(name)) {
|
|
753
|
+
issues.push(createValidationIssue(packageName, name, "skill name must be hyphen-case, lowercase, <=64 chars, and cannot contain consecutive hyphens", sourceKind));
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
skills.push({ name, path });
|
|
757
|
+
}
|
|
758
|
+
return { skills, issues };
|
|
759
|
+
}
|
|
760
|
+
function packageNameFromSpecifier(specifier) {
|
|
761
|
+
if (specifier.startsWith("@")) {
|
|
762
|
+
const [scope, name] = specifier.split("/");
|
|
763
|
+
return name ? `${scope}/${name}` : specifier;
|
|
764
|
+
}
|
|
765
|
+
return specifier.split("/")[0] || specifier;
|
|
766
|
+
}
|
|
767
|
+
function extractModuleSpecifier(moduleEntry) {
|
|
768
|
+
if (typeof moduleEntry === "string") {
|
|
769
|
+
return moduleEntry;
|
|
770
|
+
}
|
|
771
|
+
if (Array.isArray(moduleEntry) && typeof moduleEntry[0] === "string") {
|
|
772
|
+
return moduleEntry[0];
|
|
773
|
+
}
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
function isPackageSpecifier(specifier) {
|
|
777
|
+
if (!specifier) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:")) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
function isSubPath(parent, child) {
|
|
786
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
787
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
788
|
+
}
|
|
789
|
+
function readRepositoryUrl(repository) {
|
|
790
|
+
if (typeof repository === "string") return repository;
|
|
791
|
+
if (repository && typeof repository.url === "string") return repository.url;
|
|
792
|
+
return void 0;
|
|
793
|
+
}
|
|
794
|
+
async function discoverInstalledPackageFromSpecifier(specifier, rootDir) {
|
|
795
|
+
if (!isPackageSpecifier(specifier)) {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
let packageJsonPath;
|
|
799
|
+
try {
|
|
800
|
+
packageJsonPath = await resolvePackageJSON(specifier, { url: rootDir });
|
|
801
|
+
} catch {
|
|
802
|
+
const fallback = join(rootDir, "node_modules", packageNameFromSpecifier(specifier), "package.json");
|
|
803
|
+
if (!existsSync(fallback)) return null;
|
|
804
|
+
packageJsonPath = fallback;
|
|
805
|
+
}
|
|
806
|
+
return readInstalledPackageMetadata(packageJsonPath, specifier);
|
|
807
|
+
}
|
|
808
|
+
async function discoverInstalledPackageFromDirectory(directory) {
|
|
809
|
+
const packageJsonPath = join(directory, "package.json");
|
|
810
|
+
if (!await pathExists(packageJsonPath)) {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
return readInstalledPackageMetadata(packageJsonPath);
|
|
814
|
+
}
|
|
815
|
+
async function readInstalledPackageMetadata(packageJsonPath, fallbackSpecifier) {
|
|
816
|
+
const pkg = await readPackageJSON(packageJsonPath);
|
|
817
|
+
return {
|
|
818
|
+
packageName: pkg.name || (fallbackSpecifier ? packageNameFromSpecifier(fallbackSpecifier) : basename(dirname(packageJsonPath))),
|
|
819
|
+
version: pkg.version,
|
|
820
|
+
description: pkg.description,
|
|
821
|
+
packageRoot: dirname(packageJsonPath),
|
|
822
|
+
repository: readRepositoryUrl(pkg.repository),
|
|
823
|
+
homepage: pkg.homepage,
|
|
824
|
+
packageData: pkg
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function discoverPackageSkillsFromInstalledPackage(installedPackage, sourceKind = "dist") {
|
|
828
|
+
const parsed = parseAgentSkillDeclarations(installedPackage.packageData, installedPackage.packageName, sourceKind);
|
|
829
|
+
const skills = parsed.skills;
|
|
830
|
+
if (!skills.length && !parsed.issues.length) {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
packageName: installedPackage.packageName,
|
|
835
|
+
version: installedPackage.version,
|
|
836
|
+
packageRoot: installedPackage.packageRoot,
|
|
837
|
+
skills,
|
|
838
|
+
issues: parsed.issues
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async function discoverLocalPackageSkills(rootDir) {
|
|
842
|
+
const packageJsonPath = join(rootDir, "package.json");
|
|
843
|
+
if (!await pathExists(packageJsonPath)) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
const pkg = await readPackageJSON(packageJsonPath);
|
|
847
|
+
const packageName = pkg.name || "local-project";
|
|
848
|
+
const parsed = parseAgentSkillDeclarations(pkg, packageName, "dist");
|
|
849
|
+
if (!parsed.skills.length && !parsed.issues.length) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
packageName,
|
|
854
|
+
version: pkg.version,
|
|
855
|
+
packageRoot: rootDir,
|
|
856
|
+
skills: parsed.skills,
|
|
857
|
+
issues: parsed.issues
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function normalizeSkillName(rawSkillName, sourceDir) {
|
|
861
|
+
return rawSkillName?.trim() || basename(sourceDir).trim();
|
|
862
|
+
}
|
|
863
|
+
function normalizeContribution(contribution, sourceDir, sourceRoot) {
|
|
864
|
+
return {
|
|
865
|
+
packageName: contribution.packageName,
|
|
866
|
+
version: contribution.version,
|
|
867
|
+
sourceDir,
|
|
868
|
+
sourceRoot,
|
|
869
|
+
skillName: normalizeSkillName(contribution.skillName, sourceDir),
|
|
870
|
+
sourceKind: contribution.sourceKind || "dist",
|
|
871
|
+
description: contribution.description,
|
|
872
|
+
sourceRepo: contribution.sourceRepo,
|
|
873
|
+
sourceRef: contribution.sourceRef,
|
|
874
|
+
sourcePath: contribution.sourcePath,
|
|
875
|
+
repoUrl: contribution.repoUrl,
|
|
876
|
+
docsUrl: contribution.docsUrl,
|
|
877
|
+
official: contribution.official ?? true,
|
|
878
|
+
resolver: contribution.resolver || "agentsField",
|
|
879
|
+
forceIncludeScripts: contribution.forceIncludeScripts ?? false
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
async function validateContribution(contribution) {
|
|
883
|
+
const issues = [];
|
|
884
|
+
if (!isSubPath(contribution.sourceRoot, contribution.sourceDir)) {
|
|
885
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, "skill path must stay within package root", contribution.sourceKind));
|
|
886
|
+
}
|
|
887
|
+
if (!isValidSkillName(contribution.skillName)) {
|
|
888
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, "skill name must be hyphen-case, lowercase, <=64 chars, and cannot contain consecutive hyphens", contribution.sourceKind));
|
|
889
|
+
}
|
|
890
|
+
const sourceDirName = basename(contribution.sourceDir);
|
|
891
|
+
if (sourceDirName !== contribution.skillName) {
|
|
892
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, `skill directory name "${sourceDirName}" must match declared skill name`, contribution.sourceKind));
|
|
893
|
+
}
|
|
894
|
+
const skillFilePath = join(contribution.sourceDir, "SKILL.md");
|
|
895
|
+
if (!await pathExists(skillFilePath)) {
|
|
896
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, "SKILL.md is required at skill root", contribution.sourceKind));
|
|
897
|
+
return { contribution, issues };
|
|
898
|
+
}
|
|
899
|
+
const skillContents = await promises.readFile(skillFilePath, "utf8");
|
|
900
|
+
if (!matter.test(skillContents)) {
|
|
901
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, "SKILL.md must include YAML frontmatter", contribution.sourceKind));
|
|
902
|
+
return { contribution, issues };
|
|
903
|
+
}
|
|
904
|
+
let frontmatter;
|
|
905
|
+
try {
|
|
906
|
+
const parsed = matter(skillContents).data;
|
|
907
|
+
frontmatter = {
|
|
908
|
+
name: typeof parsed.name === "string" ? parsed.name.trim() : void 0,
|
|
909
|
+
description: typeof parsed.description === "string" ? parsed.description.trim() : void 0
|
|
910
|
+
};
|
|
911
|
+
} catch {
|
|
912
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, "SKILL.md must include YAML frontmatter", contribution.sourceKind));
|
|
913
|
+
return { contribution, issues };
|
|
914
|
+
}
|
|
915
|
+
if (!frontmatter.name) {
|
|
916
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, 'SKILL.md frontmatter must include non-empty "name"', contribution.sourceKind));
|
|
917
|
+
} else if (frontmatter.name !== contribution.skillName) {
|
|
918
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, `SKILL.md frontmatter name "${frontmatter.name}" must match declared skill name`, contribution.sourceKind));
|
|
919
|
+
}
|
|
920
|
+
if (!frontmatter.description) {
|
|
921
|
+
issues.push(createValidationIssue(contribution.packageName, contribution.skillName, 'SKILL.md frontmatter must include non-empty "description"', contribution.sourceKind));
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
contribution: {
|
|
925
|
+
...contribution,
|
|
926
|
+
description: frontmatter.description || contribution.description
|
|
927
|
+
},
|
|
928
|
+
issues
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async function validateResolvedContributions(contributions) {
|
|
932
|
+
const valid = [];
|
|
933
|
+
const issues = [];
|
|
934
|
+
for (const contribution of contributions) {
|
|
935
|
+
const validation = await validateContribution(contribution);
|
|
936
|
+
if (!validation.issues.length) {
|
|
937
|
+
valid.push(validation.contribution);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
issues.push(...validation.issues);
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
contributions: sortAndDedupeContributions(valid),
|
|
944
|
+
issues
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
async function resolveContributions(discoveries) {
|
|
948
|
+
const contributions = [];
|
|
949
|
+
const issues = [];
|
|
950
|
+
for (const discovery of discoveries) {
|
|
951
|
+
issues.push(...discovery.issues);
|
|
952
|
+
for (const skill of discovery.skills) {
|
|
953
|
+
const sourceDir = resolve(discovery.packageRoot, skill.path);
|
|
954
|
+
contributions.push(normalizeContribution({
|
|
955
|
+
packageName: discovery.packageName,
|
|
956
|
+
version: discovery.version,
|
|
957
|
+
skillName: skill.name
|
|
958
|
+
}, sourceDir, discovery.packageRoot));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const validated = await validateResolvedContributions(contributions);
|
|
962
|
+
return {
|
|
963
|
+
contributions: validated.contributions,
|
|
964
|
+
issues: [...issues, ...validated.issues]
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function sortAndDedupeContributions(contributions) {
|
|
968
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
969
|
+
for (const contribution of contributions) {
|
|
970
|
+
const key = [
|
|
971
|
+
contribution.packageName,
|
|
972
|
+
contribution.skillName,
|
|
973
|
+
contribution.sourceDir
|
|
974
|
+
].join("::");
|
|
975
|
+
byKey.set(key, contribution);
|
|
976
|
+
}
|
|
977
|
+
return Array.from(byKey.values()).sort((a, b) => {
|
|
978
|
+
const left = `${a.packageName}::${a.skillName}::${a.sourceDir}`;
|
|
979
|
+
const right = `${b.packageName}::${b.skillName}::${b.sourceDir}`;
|
|
980
|
+
return left.localeCompare(right);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
function sanitizeSegment(value) {
|
|
984
|
+
return value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
985
|
+
}
|
|
986
|
+
async function copySkillTree(sourceDir, destinationDir, includeScripts) {
|
|
987
|
+
await ensureDir(destinationDir);
|
|
988
|
+
await promises.cp(sourceDir, destinationDir, {
|
|
989
|
+
recursive: true,
|
|
990
|
+
force: true,
|
|
991
|
+
filter: (source) => {
|
|
992
|
+
const relativePath = relative(sourceDir, source);
|
|
993
|
+
if (!relativePath || relativePath === "") {
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
if (relativePath.split("/").includes("..")) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
if (!includeScripts && (relativePath === "scripts" || relativePath.startsWith("scripts/"))) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
return !lstatSync(source).isSymbolicLink();
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function resolveTargets(explicitTargets, rootDir) {
|
|
1007
|
+
if (explicitTargets.length) {
|
|
1008
|
+
const { valid, invalid } = validateTargets(explicitTargets);
|
|
1009
|
+
return {
|
|
1010
|
+
targets: valid,
|
|
1011
|
+
invalidTargets: invalid
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
targets: detectInstalledTargets(),
|
|
1016
|
+
invalidTargets: []
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
async function buildNuxtTemplateFiles(nuxtDir) {
|
|
1020
|
+
const nuxtRuleFiles = await loadNuxtRuleFiles();
|
|
1021
|
+
const nuxtIndexTemplate = await loadNuxtIndexTemplate();
|
|
1022
|
+
const files = Object.entries(nuxtRuleFiles).map(([name, contents]) => ({
|
|
1023
|
+
path: join(nuxtDir, name),
|
|
1024
|
+
contents
|
|
1025
|
+
}));
|
|
1026
|
+
files.push({
|
|
1027
|
+
path: join(nuxtDir, "index.template.md"),
|
|
1028
|
+
contents: nuxtIndexTemplate
|
|
1029
|
+
});
|
|
1030
|
+
return files;
|
|
1031
|
+
}
|
|
1032
|
+
async function buildVueTemplateFiles(vueDir, cacheRoot) {
|
|
1033
|
+
const vueFiles = await loadVueSkillFiles(cacheRoot);
|
|
1034
|
+
return Object.entries(vueFiles).map(([name, contents]) => ({
|
|
1035
|
+
path: join(vueDir, name),
|
|
1036
|
+
contents
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
async function renderAutomdTemplate(contents, dir) {
|
|
1040
|
+
const result = await transform(contents, { dir });
|
|
1041
|
+
return result.contents;
|
|
1042
|
+
}
|
|
1043
|
+
function createModuleDestination(baseModulesDir, contribution) {
|
|
1044
|
+
return join(
|
|
1045
|
+
baseModulesDir,
|
|
1046
|
+
sanitizeSegment(contribution.packageName),
|
|
1047
|
+
sanitizeSegment(contribution.skillName || basename(contribution.sourceDir))
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
function getTargetSkillRoot(rootDir, target, skillName) {
|
|
1051
|
+
const targetConfig = resolveAgentTargetConfig(target);
|
|
1052
|
+
if (!targetConfig) {
|
|
1053
|
+
throw new Error(`Target "${target}" could not be resolved from unagent.`);
|
|
1054
|
+
}
|
|
1055
|
+
const home = homedir();
|
|
1056
|
+
const relativeConfigDir = relative(home, targetConfig.configDir);
|
|
1057
|
+
const canMirrorConfigDir = !relativeConfigDir.startsWith("..") && !relativeConfigDir.includes("/../") && !isAbsolute(relativeConfigDir);
|
|
1058
|
+
if (canMirrorConfigDir) {
|
|
1059
|
+
const targetDir = resolve(rootDir, relativeConfigDir, targetConfig.skillsDir);
|
|
1060
|
+
const skillRoot = join(targetDir, skillName);
|
|
1061
|
+
return { targetDir, skillRoot };
|
|
1062
|
+
}
|
|
1063
|
+
throw new Error(`Target "${target}" configDir "${targetConfig.configDir}" is not under home "${home}".`);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function isCancelled(value) {
|
|
1067
|
+
return value === null || typeof value === "symbol";
|
|
1068
|
+
}
|
|
1069
|
+
async function runInstallWizard(nuxt) {
|
|
1070
|
+
const logger = useLogger("nuxt-skill-hub");
|
|
1071
|
+
if (process.env.CI || !process.stdout.isTTY) {
|
|
1072
|
+
logger.info("Non-interactive environment detected, skipping install wizard.");
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const consola = createConsola();
|
|
1076
|
+
const rootDir = nuxt.options.rootDir;
|
|
1077
|
+
const exportRoot = await resolveExportRoot(rootDir);
|
|
1078
|
+
const pkg = await readPackageJSON(rootDir).catch(() => null);
|
|
1079
|
+
const projectName = (pkg?.name || "").replace(/^@[^/]+\//, "").replace(/[^\w-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1080
|
+
let skillName = projectName ? `nuxt-${projectName}` : "nuxt";
|
|
1081
|
+
if (!isValidSkillName(skillName)) skillName = "nuxt";
|
|
1082
|
+
const pendingWrites = [];
|
|
1083
|
+
consola.box(
|
|
1084
|
+
`nuxt-skill-hub \u2014 First-time setup
|
|
1085
|
+
Let's configure AI agent skills for your Nuxt project.`
|
|
1086
|
+
);
|
|
1087
|
+
const currentTarget = detectCurrentTarget();
|
|
1088
|
+
let selectedTargets;
|
|
1089
|
+
if (currentTarget) {
|
|
1090
|
+
consola.info(`Running inside ${currentTarget}, auto-selecting it.`);
|
|
1091
|
+
selectedTargets = [currentTarget];
|
|
1092
|
+
} else {
|
|
1093
|
+
const detectedTargets = detectInstalledTargets();
|
|
1094
|
+
const allTargets = getSupportedTargets();
|
|
1095
|
+
if (!detectedTargets.length && !allTargets.length) {
|
|
1096
|
+
consola.warn("No AI agents detected. Skills will be generated when an agent is detected.");
|
|
1097
|
+
consola.info("Supported agents are sourced from unagent.");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
consola.info(`Detected agents: ${detectedTargets.length ? detectedTargets.join(", ") : "none"}`);
|
|
1101
|
+
selectedTargets = detectedTargets;
|
|
1102
|
+
if (detectedTargets.length > 1) {
|
|
1103
|
+
const keepAll = await consola.prompt("Generate skills for all detected agents?", {
|
|
1104
|
+
type: "confirm",
|
|
1105
|
+
initial: true,
|
|
1106
|
+
cancel: "null"
|
|
1107
|
+
});
|
|
1108
|
+
if (isCancelled(keepAll)) {
|
|
1109
|
+
consola.info("Setup cancelled.");
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (!keepAll) {
|
|
1113
|
+
const chosen = await consola.prompt("Select agents to generate skills for:", {
|
|
1114
|
+
type: "multiselect",
|
|
1115
|
+
options: detectedTargets.map((t) => ({ label: t, value: t })),
|
|
1116
|
+
initial: detectedTargets,
|
|
1117
|
+
required: true,
|
|
1118
|
+
cancel: "null"
|
|
1119
|
+
});
|
|
1120
|
+
if (isCancelled(chosen)) {
|
|
1121
|
+
consola.info("Setup cancelled.");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
selectedTargets = chosen.map((c) => c.value);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
|
|
1129
|
+
const uniqueSpecifiers = Array.from(new Set(moduleSpecifiers));
|
|
1130
|
+
const installedModulePackages = [];
|
|
1131
|
+
for (const specifier of uniqueSpecifiers) {
|
|
1132
|
+
const pkg2 = await discoverInstalledPackageFromSpecifier(specifier, rootDir);
|
|
1133
|
+
if (!pkg2 || pkg2.packageName === "nuxt-skill-hub")
|
|
1134
|
+
continue;
|
|
1135
|
+
installedModulePackages.push(pkg2.packageName);
|
|
1136
|
+
}
|
|
1137
|
+
if (installedModulePackages.length) {
|
|
1138
|
+
consola.info(`Detected ${installedModulePackages.length} installed module package(s):`);
|
|
1139
|
+
for (const packageName of installedModulePackages) {
|
|
1140
|
+
consola.log(` ${packageName}`);
|
|
1141
|
+
}
|
|
1142
|
+
consola.info("Module skills and metadata routers are resolved automatically at build time.");
|
|
1143
|
+
} else {
|
|
1144
|
+
consola.info("No installed module entries were discovered.");
|
|
1145
|
+
}
|
|
1146
|
+
const gitignorePath = join(exportRoot, ".gitignore");
|
|
1147
|
+
const gitignoreExists = existsSync(gitignorePath);
|
|
1148
|
+
const currentGitignore = gitignoreExists ? readFileSync(gitignorePath, "utf8") : "";
|
|
1149
|
+
const missingPatterns = [];
|
|
1150
|
+
for (const target of selectedTargets) {
|
|
1151
|
+
const { targetDir } = getTargetSkillRoot(exportRoot, target, skillName);
|
|
1152
|
+
const pattern = relative(exportRoot, targetDir).replace(/\/?$/, "/");
|
|
1153
|
+
if (!currentGitignore.includes(pattern)) {
|
|
1154
|
+
missingPatterns.push(pattern);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (missingPatterns.length) {
|
|
1158
|
+
const addGitignore = await consola.prompt(
|
|
1159
|
+
`Add generated skill directories to .gitignore?
|
|
1160
|
+
${missingPatterns.map((p) => ` ${p}`).join("\n")}`,
|
|
1161
|
+
{ type: "confirm", initial: true, cancel: "null" }
|
|
1162
|
+
);
|
|
1163
|
+
if (isCancelled(addGitignore)) {
|
|
1164
|
+
consola.info("Setup cancelled.");
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (addGitignore) {
|
|
1168
|
+
const block = `
|
|
1169
|
+
# nuxt-skill-hub (generated skills)
|
|
1170
|
+
${missingPatterns.join("\n")}
|
|
1171
|
+
`;
|
|
1172
|
+
const newContent = currentGitignore.trimEnd() + "\n" + block;
|
|
1173
|
+
pendingWrites.push({
|
|
1174
|
+
file: ".gitignore",
|
|
1175
|
+
absPath: gitignorePath,
|
|
1176
|
+
description: `Add ${missingPatterns.length} skill directory pattern(s)`,
|
|
1177
|
+
content: newContent,
|
|
1178
|
+
action: gitignoreExists ? "modify" : "create"
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const hintBlock = `${MANAGED_HINT_START}
|
|
1183
|
+
Use the \`${skillName}\` skill as the first entrypoint for Nuxt tasks in this repository.
|
|
1184
|
+
${MANAGED_HINT_END}`;
|
|
1185
|
+
const hintFileChoice = await consola.prompt("Add a skill hint to help agents discover the skill?", {
|
|
1186
|
+
type: "select",
|
|
1187
|
+
options: [
|
|
1188
|
+
{ label: "CLAUDE.md", value: "claude", hint: "Claude Code project instructions" },
|
|
1189
|
+
{ label: "AGENTS.md", value: "agents", hint: "Universal agent instructions" },
|
|
1190
|
+
{ label: "Both", value: "both" },
|
|
1191
|
+
{ label: "Skip", value: "skip" }
|
|
1192
|
+
],
|
|
1193
|
+
cancel: "null"
|
|
1194
|
+
});
|
|
1195
|
+
if (isCancelled(hintFileChoice)) {
|
|
1196
|
+
consola.info("Setup cancelled.");
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const addToFile = async (filename) => {
|
|
1200
|
+
const filePath = join(exportRoot, filename);
|
|
1201
|
+
const exists = await pathExists(filePath);
|
|
1202
|
+
const current = exists ? readFileSync(filePath, "utf8") : "";
|
|
1203
|
+
const regex = new RegExp(`${MANAGED_HINT_START}[\\s\\S]*?${MANAGED_HINT_END}`);
|
|
1204
|
+
let newContent;
|
|
1205
|
+
if (regex.test(current)) {
|
|
1206
|
+
newContent = current.replace(regex, hintBlock);
|
|
1207
|
+
} else if (current) {
|
|
1208
|
+
newContent = `${current.trimEnd()}
|
|
1209
|
+
|
|
1210
|
+
${hintBlock}
|
|
1211
|
+
`;
|
|
1212
|
+
} else {
|
|
1213
|
+
newContent = `${hintBlock}
|
|
1214
|
+
`;
|
|
1215
|
+
}
|
|
1216
|
+
pendingWrites.push({
|
|
1217
|
+
file: filename,
|
|
1218
|
+
absPath: filePath,
|
|
1219
|
+
description: "Add skill hint block",
|
|
1220
|
+
content: newContent,
|
|
1221
|
+
action: exists ? "modify" : "create"
|
|
1222
|
+
});
|
|
1223
|
+
};
|
|
1224
|
+
if (hintFileChoice === "claude" || hintFileChoice === "both") {
|
|
1225
|
+
await addToFile("CLAUDE.md");
|
|
1226
|
+
}
|
|
1227
|
+
if (hintFileChoice === "agents" || hintFileChoice === "both") {
|
|
1228
|
+
await addToFile("AGENTS.md");
|
|
1229
|
+
}
|
|
1230
|
+
if (!pendingWrites.length) {
|
|
1231
|
+
consola.success("No file changes needed. Setup complete!");
|
|
1232
|
+
consola.info("Run `nuxt dev` or `nuxt prepare` to generate skills.");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
consola.log("");
|
|
1236
|
+
consola.info("Planned changes:");
|
|
1237
|
+
for (const write of pendingWrites) {
|
|
1238
|
+
const icon = write.action === "create" ? "+" : "~";
|
|
1239
|
+
consola.log(` ${icon} ${write.file} \u2014 ${write.description}`);
|
|
1240
|
+
}
|
|
1241
|
+
consola.log("");
|
|
1242
|
+
const confirm = await consola.prompt("Apply these changes?", {
|
|
1243
|
+
type: "confirm",
|
|
1244
|
+
initial: true,
|
|
1245
|
+
cancel: "null"
|
|
1246
|
+
});
|
|
1247
|
+
if (isCancelled(confirm) || !confirm) {
|
|
1248
|
+
consola.info("Setup cancelled. No files were modified.");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
for (const write of pendingWrites) {
|
|
1252
|
+
writeFileSync(write.absPath, write.content, "utf8");
|
|
1253
|
+
const verb = write.action === "create" ? "Created" : "Updated";
|
|
1254
|
+
consola.success(`${verb} ${write.file}`);
|
|
1255
|
+
}
|
|
1256
|
+
consola.log("");
|
|
1257
|
+
consola.box(
|
|
1258
|
+
`Setup complete!
|
|
1259
|
+
|
|
1260
|
+
Run \`nuxt dev\` or \`nuxt prepare\` to generate skills.
|
|
1261
|
+
Configure \`skillHub.skillName\` or \`skillHub.targets\` in nuxt.config.ts if needed.`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const GITHUB_OVERRIDES = [
|
|
1266
|
+
{
|
|
1267
|
+
packageName: "@nuxt/ui",
|
|
1268
|
+
repo: "nuxt/ui",
|
|
1269
|
+
ref: "v4",
|
|
1270
|
+
path: "skills/nuxt-ui",
|
|
1271
|
+
skillName: "nuxt-ui"
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
packageName: "@vueuse/core",
|
|
1275
|
+
repo: "vueuse/vueuse",
|
|
1276
|
+
ref: "main",
|
|
1277
|
+
path: "skills/vueuse-functions",
|
|
1278
|
+
skillName: "vueuse-functions"
|
|
1279
|
+
}
|
|
1280
|
+
];
|
|
1281
|
+
function findGitHubOverride(packageName) {
|
|
1282
|
+
return GITHUB_OVERRIDES.find((entry) => entry.packageName === packageName);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const GITHUB_API_BASE = "https://api.github.com";
|
|
1286
|
+
const UNGH_API_BASE = "https://ungh.cc";
|
|
1287
|
+
function githubFetchOptions(timeoutMs) {
|
|
1288
|
+
return {
|
|
1289
|
+
timeout: timeoutMs,
|
|
1290
|
+
headers: {
|
|
1291
|
+
"Accept": "application/json",
|
|
1292
|
+
"User-Agent": "nuxt-skill-hub"
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function encodeGitHubPath(path) {
|
|
1297
|
+
return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
1298
|
+
}
|
|
1299
|
+
function toRepoPath(value) {
|
|
1300
|
+
if (!value) {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
const trimmed = value.trim();
|
|
1304
|
+
const direct = trimmed.replace(/^git\+/, "").replace(/^https?:\/\/github\.com\//, "").replace(/^git:\/\/github\.com\//, "").replace(/^git@github\.com:/, "").replace(/\/+$/, "").replace(/\.git$/, "");
|
|
1305
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(direct)) {
|
|
1306
|
+
return direct;
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
const url = new URL(trimmed.replace(/^git\+/, ""));
|
|
1310
|
+
if (url.hostname !== "github.com") {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const pathname = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1314
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(pathname)) {
|
|
1315
|
+
return pathname;
|
|
1316
|
+
}
|
|
1317
|
+
} catch {
|
|
1318
|
+
}
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
function parseGitHubRepo(input) {
|
|
1322
|
+
if (!input) {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
return toRepoPath(input);
|
|
1326
|
+
}
|
|
1327
|
+
async function listGitHubDirectory(repo, ref, dirPath, timeoutMs) {
|
|
1328
|
+
const repoPath = toRepoPath(repo);
|
|
1329
|
+
if (!repoPath) {
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
const normalizedDirPath = dirPath.replace(/^\/+|\/+$/g, "");
|
|
1333
|
+
const prefix = normalizedDirPath ? `${normalizedDirPath}/` : "";
|
|
1334
|
+
try {
|
|
1335
|
+
const data = await ofetch(
|
|
1336
|
+
`${GITHUB_API_BASE}/repos/${repoPath}/git/trees/${encodeURIComponent(ref)}?recursive=1`,
|
|
1337
|
+
githubFetchOptions(timeoutMs)
|
|
1338
|
+
);
|
|
1339
|
+
const entries = /* @__PURE__ */ new Set();
|
|
1340
|
+
for (const entry of data.tree || []) {
|
|
1341
|
+
if (entry.type !== "tree" || !entry.path.startsWith(prefix)) {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
const remainder = entry.path.slice(prefix.length);
|
|
1345
|
+
if (remainder && !remainder.includes("/")) {
|
|
1346
|
+
entries.add(remainder);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return Array.from(entries).sort((a, b) => a.localeCompare(b));
|
|
1350
|
+
} catch {
|
|
1351
|
+
return [];
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function fetchGitHubDefaultBranch(repo, timeoutMs) {
|
|
1355
|
+
const repoPath = toRepoPath(repo);
|
|
1356
|
+
if (!repoPath) {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
const [owner, name] = repoPath.split("/");
|
|
1360
|
+
if (!owner || !name) {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
try {
|
|
1364
|
+
const data = await ofetch(
|
|
1365
|
+
`${UNGH_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
|
|
1366
|
+
githubFetchOptions(timeoutMs)
|
|
1367
|
+
);
|
|
1368
|
+
return data.repo?.defaultBranch || null;
|
|
1369
|
+
} catch {
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
async function fetchGitHubFileText(repo, ref, filePath, timeoutMs) {
|
|
1374
|
+
const repoPath = toRepoPath(repo);
|
|
1375
|
+
if (!repoPath) {
|
|
1376
|
+
return { ok: false, error: "Invalid GitHub repository format" };
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
const data = await ofetch(
|
|
1380
|
+
`https://raw.githubusercontent.com/${repoPath}/${encodeURIComponent(ref)}/${encodeGitHubPath(filePath)}`,
|
|
1381
|
+
{
|
|
1382
|
+
...githubFetchOptions(timeoutMs),
|
|
1383
|
+
responseType: "text"
|
|
1384
|
+
}
|
|
1385
|
+
);
|
|
1386
|
+
return { ok: true, data };
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
const status = typeof error === "object" && error && "status" in error ? Number(error.status) : void 0;
|
|
1389
|
+
return {
|
|
1390
|
+
ok: false,
|
|
1391
|
+
status,
|
|
1392
|
+
error: error.message
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function makeSkip(packageName, skillName, reason, sourceKind) {
|
|
1398
|
+
return {
|
|
1399
|
+
packageName,
|
|
1400
|
+
skillName,
|
|
1401
|
+
reason,
|
|
1402
|
+
sourceKind
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function slugCandidates(packageName) {
|
|
1406
|
+
const [scopeOrName, scopedName] = packageName.startsWith("@") ? packageName.split("/") : ["", packageName];
|
|
1407
|
+
const base = (scopedName || scopeOrName || "").trim();
|
|
1408
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1409
|
+
if (base) {
|
|
1410
|
+
candidates.add(base);
|
|
1411
|
+
}
|
|
1412
|
+
if (scopeOrName === "@nuxt" && base) {
|
|
1413
|
+
candidates.add(`nuxt-${base}`);
|
|
1414
|
+
}
|
|
1415
|
+
if (scopeOrName === "@nuxthub" && base === "core") {
|
|
1416
|
+
candidates.add("nuxthub");
|
|
1417
|
+
}
|
|
1418
|
+
if (scopeOrName === "@vueuse" && base === "core") {
|
|
1419
|
+
candidates.add("vueuse");
|
|
1420
|
+
}
|
|
1421
|
+
return Array.from(candidates);
|
|
1422
|
+
}
|
|
1423
|
+
function dedupe(values) {
|
|
1424
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1425
|
+
const output = [];
|
|
1426
|
+
for (const value of values) {
|
|
1427
|
+
if (!value || seen.has(value)) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
seen.add(value);
|
|
1431
|
+
output.push(value);
|
|
1432
|
+
}
|
|
1433
|
+
return output;
|
|
1434
|
+
}
|
|
1435
|
+
function normalizeHttpUrl(url) {
|
|
1436
|
+
const trimmed = url?.trim();
|
|
1437
|
+
if (!trimmed) {
|
|
1438
|
+
return void 0;
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
const parsed = new URL(trimmed);
|
|
1442
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString() : void 0;
|
|
1443
|
+
} catch {
|
|
1444
|
+
return void 0;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function resolveRepositoryUrl(packageInfo) {
|
|
1448
|
+
const sourceRepo = parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
|
|
1449
|
+
if (sourceRepo) {
|
|
1450
|
+
return {
|
|
1451
|
+
sourceRepo,
|
|
1452
|
+
repoUrl: `https://github.com/${sourceRepo}`
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
repoUrl: normalizeHttpUrl(packageInfo.repository)
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
async function materializeCandidate(packageInfo, candidate, cacheRoot) {
|
|
1460
|
+
const targetDir = join(
|
|
1461
|
+
cacheRoot,
|
|
1462
|
+
candidate.sourceKind,
|
|
1463
|
+
sanitizeSegment(candidate.sourceRepo),
|
|
1464
|
+
sanitizeSegment(candidate.sourceRef),
|
|
1465
|
+
sanitizeSegment(packageInfo.packageName),
|
|
1466
|
+
sanitizeSegment(candidate.skillName)
|
|
1467
|
+
);
|
|
1468
|
+
try {
|
|
1469
|
+
await downloadTemplate(`gh:${candidate.sourceRepo}/${candidate.sourcePath}#${candidate.sourceRef}`, {
|
|
1470
|
+
dir: targetDir,
|
|
1471
|
+
force: true,
|
|
1472
|
+
forceClean: true,
|
|
1473
|
+
registry: false,
|
|
1474
|
+
silent: true
|
|
1475
|
+
});
|
|
1476
|
+
} catch {
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
if (!await pathExists(join(targetDir, "SKILL.md"))) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
return normalizeContribution({
|
|
1483
|
+
packageName: packageInfo.packageName,
|
|
1484
|
+
version: packageInfo.version,
|
|
1485
|
+
skillName: candidate.skillName,
|
|
1486
|
+
sourceKind: candidate.sourceKind,
|
|
1487
|
+
sourceRepo: candidate.sourceRepo,
|
|
1488
|
+
sourceRef: candidate.sourceRef,
|
|
1489
|
+
sourcePath: candidate.sourcePath,
|
|
1490
|
+
official: candidate.official,
|
|
1491
|
+
resolver: candidate.resolver,
|
|
1492
|
+
forceIncludeScripts: true
|
|
1493
|
+
}, targetDir, join(targetDir, ".."));
|
|
1494
|
+
}
|
|
1495
|
+
async function resolveViaGitHub(packageInfo, cacheRoot, timeoutMs) {
|
|
1496
|
+
const issues = [];
|
|
1497
|
+
const skipped = [];
|
|
1498
|
+
const override = findGitHubOverride(packageInfo.packageName);
|
|
1499
|
+
const repo = override?.repo || parseGitHubRepo(packageInfo.repository) || parseGitHubRepo(packageInfo.homepage);
|
|
1500
|
+
if (!repo) {
|
|
1501
|
+
return {
|
|
1502
|
+
contributions: [],
|
|
1503
|
+
issues,
|
|
1504
|
+
skipped: [makeSkip(packageInfo.packageName, override?.skillName || packageInfo.packageName, "No GitHub repository metadata found", "github")]
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
const refs = dedupe([
|
|
1508
|
+
override?.ref || "",
|
|
1509
|
+
packageInfo.version ? `v${packageInfo.version}` : "",
|
|
1510
|
+
packageInfo.version || "",
|
|
1511
|
+
await fetchGitHubDefaultBranch(repo, timeoutMs) || "",
|
|
1512
|
+
"main",
|
|
1513
|
+
"master"
|
|
1514
|
+
]);
|
|
1515
|
+
const candidates = dedupe([
|
|
1516
|
+
override?.skillName || "",
|
|
1517
|
+
...slugCandidates(packageInfo.packageName)
|
|
1518
|
+
]);
|
|
1519
|
+
const pathCandidates = dedupe([
|
|
1520
|
+
override?.path || "",
|
|
1521
|
+
...candidates.flatMap((skillName) => {
|
|
1522
|
+
return [
|
|
1523
|
+
`skills/${skillName}`,
|
|
1524
|
+
`.claude/skills/${skillName}`,
|
|
1525
|
+
`.github/skills/${skillName}`
|
|
1526
|
+
];
|
|
1527
|
+
})
|
|
1528
|
+
]);
|
|
1529
|
+
for (const ref of refs) {
|
|
1530
|
+
const packageJson = await fetchGitHubFileText(repo, ref, "package.json", timeoutMs);
|
|
1531
|
+
if (packageJson.ok && packageJson.data) {
|
|
1532
|
+
try {
|
|
1533
|
+
const parsed = JSON.parse(packageJson.data);
|
|
1534
|
+
const remoteSkills = parseAgentSkillDeclarations(parsed, packageInfo.packageName, "github");
|
|
1535
|
+
issues.push(...remoteSkills.issues);
|
|
1536
|
+
for (const skill of remoteSkills.skills) {
|
|
1537
|
+
const resolved = await materializeCandidate(packageInfo, {
|
|
1538
|
+
skillName: skill.name,
|
|
1539
|
+
sourcePath: skill.path,
|
|
1540
|
+
sourceKind: "github",
|
|
1541
|
+
sourceRepo: repo,
|
|
1542
|
+
sourceRef: ref,
|
|
1543
|
+
official: true,
|
|
1544
|
+
resolver: "agentsField"
|
|
1545
|
+
}, cacheRoot);
|
|
1546
|
+
if (resolved) {
|
|
1547
|
+
return { contributions: [resolved], issues, skipped };
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
} catch {
|
|
1551
|
+
issues.push(createValidationIssue(packageInfo.packageName, packageInfo.packageName, "Failed to parse remote package.json", "github"));
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const skillsDirs = await listGitHubDirectory(repo, ref, "skills", timeoutMs);
|
|
1555
|
+
if (skillsDirs.length) {
|
|
1556
|
+
const contributions = [];
|
|
1557
|
+
for (const skillName of skillsDirs) {
|
|
1558
|
+
const resolved = await materializeCandidate(packageInfo, {
|
|
1559
|
+
skillName,
|
|
1560
|
+
sourcePath: `skills/${skillName}`,
|
|
1561
|
+
sourceKind: "github",
|
|
1562
|
+
sourceRepo: repo,
|
|
1563
|
+
sourceRef: ref,
|
|
1564
|
+
official: true,
|
|
1565
|
+
resolver: "githubHeuristic"
|
|
1566
|
+
}, cacheRoot);
|
|
1567
|
+
if (resolved) {
|
|
1568
|
+
contributions.push(resolved);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (contributions.length) {
|
|
1572
|
+
return { contributions, issues, skipped };
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
for (const path of pathCandidates) {
|
|
1576
|
+
const candidateSkillName = override?.path && path === override.path ? override.skillName || path.split("/").pop() || packageInfo.packageName : path.split("/").pop() || packageInfo.packageName;
|
|
1577
|
+
const resolved = await materializeCandidate(packageInfo, {
|
|
1578
|
+
skillName: candidateSkillName,
|
|
1579
|
+
sourcePath: path,
|
|
1580
|
+
sourceKind: "github",
|
|
1581
|
+
sourceRepo: repo,
|
|
1582
|
+
sourceRef: ref,
|
|
1583
|
+
official: true,
|
|
1584
|
+
resolver: "githubHeuristic"
|
|
1585
|
+
}, cacheRoot);
|
|
1586
|
+
if (resolved) {
|
|
1587
|
+
return { contributions: [resolved], issues, skipped };
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
skipped.push(makeSkip(packageInfo.packageName, override?.skillName || packageInfo.packageName, "No GitHub skill found using configured refs and path heuristics", "github"));
|
|
1592
|
+
return {
|
|
1593
|
+
contributions: [],
|
|
1594
|
+
issues,
|
|
1595
|
+
skipped
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
async function resolveViaMetadataRouter(packageInfo, cacheRoot) {
|
|
1599
|
+
const { repoUrl, sourceRepo } = resolveRepositoryUrl(packageInfo);
|
|
1600
|
+
const docsUrl = normalizeHttpUrl(packageInfo.homepage);
|
|
1601
|
+
if (!repoUrl && !docsUrl) {
|
|
1602
|
+
return {
|
|
1603
|
+
contributions: [],
|
|
1604
|
+
issues: [],
|
|
1605
|
+
skipped: [makeSkip(packageInfo.packageName, packageInfo.packageName, "No package metadata was available to generate a metadata-routed skill", "generated")]
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
const skillName = resolveMetadataRouterSkillName(packageInfo.packageName);
|
|
1609
|
+
const targetDir = join(
|
|
1610
|
+
cacheRoot,
|
|
1611
|
+
"generated",
|
|
1612
|
+
"metadata-router",
|
|
1613
|
+
sanitizeSegment(packageInfo.packageName),
|
|
1614
|
+
sanitizeSegment(skillName)
|
|
1615
|
+
);
|
|
1616
|
+
const files = createMetadataRouterSkillFiles({
|
|
1617
|
+
packageName: packageInfo.packageName,
|
|
1618
|
+
skillName,
|
|
1619
|
+
description: packageInfo.description,
|
|
1620
|
+
repoUrl,
|
|
1621
|
+
docsUrl
|
|
1622
|
+
});
|
|
1623
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
1624
|
+
const destination = join(targetDir, relativePath);
|
|
1625
|
+
await ensureDir(dirname(destination));
|
|
1626
|
+
await promises.writeFile(destination, contents, "utf8");
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
contributions: [
|
|
1630
|
+
normalizeContribution({
|
|
1631
|
+
packageName: packageInfo.packageName,
|
|
1632
|
+
version: packageInfo.version,
|
|
1633
|
+
skillName,
|
|
1634
|
+
description: packageInfo.description,
|
|
1635
|
+
sourceKind: "generated",
|
|
1636
|
+
sourceRepo,
|
|
1637
|
+
repoUrl,
|
|
1638
|
+
docsUrl,
|
|
1639
|
+
official: true,
|
|
1640
|
+
resolver: "metadataRouter",
|
|
1641
|
+
forceIncludeScripts: false
|
|
1642
|
+
}, targetDir, join(targetDir, ".."))
|
|
1643
|
+
],
|
|
1644
|
+
issues: [],
|
|
1645
|
+
skipped: []
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
async function resolveRemoteContributionsForPackage(packageInfo, options) {
|
|
1649
|
+
const githubResult = options.enableGithubLookup ? await resolveViaGitHub(packageInfo, options.cacheRoot, options.githubLookupTimeoutMs) : {
|
|
1650
|
+
contributions: [],
|
|
1651
|
+
issues: [],
|
|
1652
|
+
skipped: []
|
|
1653
|
+
};
|
|
1654
|
+
if (githubResult.contributions.length) {
|
|
1655
|
+
return githubResult;
|
|
1656
|
+
}
|
|
1657
|
+
const generatedResult = await resolveViaMetadataRouter(packageInfo, options.cacheRoot);
|
|
1658
|
+
if (generatedResult.contributions.length) {
|
|
1659
|
+
return {
|
|
1660
|
+
contributions: generatedResult.contributions,
|
|
1661
|
+
issues: [...githubResult.issues, ...generatedResult.issues],
|
|
1662
|
+
skipped: generatedResult.skipped
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
contributions: [],
|
|
1667
|
+
issues: [...githubResult.issues, ...generatedResult.issues],
|
|
1668
|
+
skipped: [...githubResult.skipped, ...generatedResult.skipped]
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const GITHUB_LOOKUP_TIMEOUT_MS = 1500;
|
|
1673
|
+
async function resolveManualContribution(rootDir, contribution) {
|
|
1674
|
+
const sourceDir = isAbsolute(contribution.sourceDir) ? contribution.sourceDir : resolve(rootDir, contribution.sourceDir);
|
|
1675
|
+
return normalizeContribution(contribution, sourceDir, sourceDir);
|
|
1676
|
+
}
|
|
1677
|
+
function issuesToSkipped(issues) {
|
|
1678
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1679
|
+
for (const issue of issues) {
|
|
1680
|
+
const key = `${issue.packageName}::${issue.skillName}`;
|
|
1681
|
+
const previous = byKey.get(key);
|
|
1682
|
+
if (!previous) {
|
|
1683
|
+
byKey.set(key, {
|
|
1684
|
+
packageName: issue.packageName,
|
|
1685
|
+
skillName: issue.skillName,
|
|
1686
|
+
reason: issue.reason,
|
|
1687
|
+
sourceKind: issue.sourceKind
|
|
1688
|
+
});
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
const reasons = new Set(previous.reason.split("; ").filter(Boolean));
|
|
1692
|
+
reasons.add(issue.reason);
|
|
1693
|
+
previous.reason = Array.from(reasons).join("; ");
|
|
1694
|
+
}
|
|
1695
|
+
return Array.from(byKey.values()).sort((a, b) => `${a.packageName}::${a.skillName}`.localeCompare(`${b.packageName}::${b.skillName}`));
|
|
1696
|
+
}
|
|
1697
|
+
function mergeSkippedEntries(entries) {
|
|
1698
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1699
|
+
for (const entry of entries) {
|
|
1700
|
+
const key = `${entry.packageName}::${entry.skillName}::${entry.sourceKind || ""}`;
|
|
1701
|
+
const previous = byKey.get(key);
|
|
1702
|
+
if (!previous) {
|
|
1703
|
+
byKey.set(key, { ...entry });
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const reasons = new Set(previous.reason.split("; ").filter(Boolean));
|
|
1707
|
+
reasons.add(entry.reason);
|
|
1708
|
+
previous.reason = Array.from(reasons).join("; ");
|
|
1709
|
+
}
|
|
1710
|
+
return Array.from(byKey.values()).sort((a, b) => `${a.packageName}::${a.skillName}`.localeCompare(`${b.packageName}::${b.skillName}`));
|
|
1711
|
+
}
|
|
1712
|
+
const module$1 = defineNuxtModule({
|
|
1713
|
+
meta: {
|
|
1714
|
+
name: "nuxt-skill-hub",
|
|
1715
|
+
version: PACKAGE_VERSION,
|
|
1716
|
+
configKey: "skillHub",
|
|
1717
|
+
compatibility: {
|
|
1718
|
+
nuxt: ">=4.3.0"
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
async onInstall(nuxt) {
|
|
1722
|
+
await runInstallWizard(nuxt);
|
|
1723
|
+
},
|
|
1724
|
+
defaults: {
|
|
1725
|
+
skillName: "",
|
|
1726
|
+
targets: [],
|
|
1727
|
+
moduleAuthoring: false
|
|
1728
|
+
},
|
|
1729
|
+
async setup(options, nuxt) {
|
|
1730
|
+
const logger = useLogger("nuxt-skill-hub");
|
|
1731
|
+
const configuredSkillName = options.skillName?.trim();
|
|
1732
|
+
let resolvedSkillName;
|
|
1733
|
+
if (configuredSkillName && isValidSkillName(configuredSkillName)) {
|
|
1734
|
+
resolvedSkillName = configuredSkillName;
|
|
1735
|
+
} else {
|
|
1736
|
+
if (configuredSkillName) {
|
|
1737
|
+
logger.warn(`Invalid skillHub.skillName "${configuredSkillName}". Deriving from package.json.`);
|
|
1738
|
+
}
|
|
1739
|
+
const pkg = await readPackageJSON(nuxt.options.rootDir).catch(() => null);
|
|
1740
|
+
const projectName = (pkg?.name || "").replace(/^@[^/]+\//, "").replace(/[^\w-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1741
|
+
resolvedSkillName = projectName ? `nuxt-${projectName}` : "nuxt";
|
|
1742
|
+
if (!isValidSkillName(resolvedSkillName)) {
|
|
1743
|
+
resolvedSkillName = "nuxt";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
nuxt.hook("modules:done", async () => {
|
|
1747
|
+
const exportRoot = await resolveExportRoot(nuxt.options.rootDir);
|
|
1748
|
+
const monorepoScopePath = resolveMonorepoScopePath(nuxt.options.rootDir, exportRoot);
|
|
1749
|
+
const manualContributions = [];
|
|
1750
|
+
const contributionContext = {
|
|
1751
|
+
add: (contribution) => {
|
|
1752
|
+
manualContributions.push(contribution);
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
const callSkillContributeHook = nuxt.callHook;
|
|
1756
|
+
await callSkillContributeHook("skill-hub:contribute", contributionContext);
|
|
1757
|
+
const discoveries = [];
|
|
1758
|
+
const installedPackages = [];
|
|
1759
|
+
const seenPackageRoots = /* @__PURE__ */ new Set();
|
|
1760
|
+
const addInstalledPackage = (installedPackage) => {
|
|
1761
|
+
if (!installedPackage || seenPackageRoots.has(installedPackage.packageRoot)) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
seenPackageRoots.add(installedPackage.packageRoot);
|
|
1765
|
+
installedPackages.push(installedPackage);
|
|
1766
|
+
const discovered = discoverPackageSkillsFromInstalledPackage(installedPackage, "dist");
|
|
1767
|
+
if (discovered) {
|
|
1768
|
+
discoveries.push(discovered);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
|
|
1772
|
+
const seenSpecifiers = Array.from(new Set(moduleSpecifiers));
|
|
1773
|
+
for (const specifier of seenSpecifiers) {
|
|
1774
|
+
addInstalledPackage(await discoverInstalledPackageFromSpecifier(specifier, nuxt.options.rootDir));
|
|
1775
|
+
}
|
|
1776
|
+
const layerDirectories = Array.from(new Set(
|
|
1777
|
+
nuxt.options._layers.map((layer) => resolve(layer.cwd || layer.config.rootDir || "")).filter((layerDir) => layerDir && layerDir !== resolve(nuxt.options.rootDir))
|
|
1778
|
+
));
|
|
1779
|
+
for (const layerDirectory of layerDirectories) {
|
|
1780
|
+
addInstalledPackage(await discoverInstalledPackageFromDirectory(layerDirectory));
|
|
1781
|
+
}
|
|
1782
|
+
const localPackageSkills = await discoverLocalPackageSkills(nuxt.options.rootDir);
|
|
1783
|
+
if (localPackageSkills) {
|
|
1784
|
+
discoveries.push(localPackageSkills);
|
|
1785
|
+
}
|
|
1786
|
+
const discoveredContributions = await resolveContributions(discoveries);
|
|
1787
|
+
const distResolvedPackages = new Set(discoveredContributions.contributions.map((item) => item.packageName));
|
|
1788
|
+
const remoteIssues = [];
|
|
1789
|
+
const remoteSkipped = [];
|
|
1790
|
+
const remoteContributions = [];
|
|
1791
|
+
const remoteCacheRoot = join(nuxt.options.rootDir, ".nuxt", "skill-hub-cache");
|
|
1792
|
+
await emptyDir(remoteCacheRoot);
|
|
1793
|
+
for (const pkg of installedPackages) {
|
|
1794
|
+
if (pkg.packageName === "nuxt-skill-hub") {
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (distResolvedPackages.has(pkg.packageName)) {
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
const remote = await resolveRemoteContributionsForPackage(pkg, {
|
|
1801
|
+
cacheRoot: remoteCacheRoot,
|
|
1802
|
+
githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
|
|
1803
|
+
enableGithubLookup: true
|
|
1804
|
+
});
|
|
1805
|
+
remoteIssues.push(...remote.issues);
|
|
1806
|
+
remoteSkipped.push(...remote.skipped);
|
|
1807
|
+
remoteContributions.push(...remote.contributions);
|
|
1808
|
+
}
|
|
1809
|
+
const validatedRemote = await validateResolvedContributions(sortAndDedupeContributions(remoteContributions));
|
|
1810
|
+
const resolvedManual = await Promise.all(
|
|
1811
|
+
manualContributions.map((contribution) => resolveManualContribution(nuxt.options.rootDir, contribution))
|
|
1812
|
+
);
|
|
1813
|
+
const validatedManual = await validateResolvedContributions(sortAndDedupeContributions(resolvedManual));
|
|
1814
|
+
const contributions = sortAndDedupeContributions([
|
|
1815
|
+
...discoveredContributions.contributions,
|
|
1816
|
+
...validatedRemote.contributions,
|
|
1817
|
+
...validatedManual.contributions
|
|
1818
|
+
]);
|
|
1819
|
+
const validationIssues = [
|
|
1820
|
+
...discoveredContributions.issues,
|
|
1821
|
+
...remoteIssues,
|
|
1822
|
+
...validatedRemote.issues,
|
|
1823
|
+
...validatedManual.issues
|
|
1824
|
+
];
|
|
1825
|
+
const skipped = mergeSkippedEntries([
|
|
1826
|
+
...issuesToSkipped(validationIssues),
|
|
1827
|
+
...remoteSkipped
|
|
1828
|
+
]);
|
|
1829
|
+
for (const issue of validationIssues) {
|
|
1830
|
+
logger.warn(`[validation] ${issue.packageName}/${issue.skillName}: ${issue.reason}`);
|
|
1831
|
+
}
|
|
1832
|
+
const targetResolution = resolveTargets(
|
|
1833
|
+
options.targets || [],
|
|
1834
|
+
nuxt.options.rootDir
|
|
1835
|
+
);
|
|
1836
|
+
const targets = targetResolution.targets;
|
|
1837
|
+
for (const invalidTarget of targetResolution.invalidTargets) {
|
|
1838
|
+
if (invalidTarget.reason === "unknown-target") {
|
|
1839
|
+
logger.warn(`Target "${invalidTarget.target}" is unknown in unagent and was skipped.`);
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
logger.warn(`Target "${invalidTarget.target}" has no skillsDir in unagent and was skipped.`);
|
|
1843
|
+
}
|
|
1844
|
+
if (!targets.length) {
|
|
1845
|
+
logger.warn("No detected targets. Set skillHub.targets to force generation for specific agents.");
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const nuxtMetadata = await loadNuxtMetadata();
|
|
1849
|
+
for (const target of targets) {
|
|
1850
|
+
const { skillRoot } = getTargetSkillRoot(exportRoot, target, resolvedSkillName);
|
|
1851
|
+
const referencesRoot = join(skillRoot, "references");
|
|
1852
|
+
const nuxtRoot = join(referencesRoot, "nuxt");
|
|
1853
|
+
const vueRoot = join(referencesRoot, "vue");
|
|
1854
|
+
const modulesRoot = join(referencesRoot, "modules");
|
|
1855
|
+
await emptyDir(skillRoot);
|
|
1856
|
+
await ensureDir(nuxtRoot);
|
|
1857
|
+
await ensureDir(vueRoot);
|
|
1858
|
+
await ensureDir(modulesRoot);
|
|
1859
|
+
const nuxtTemplateFiles = await buildNuxtTemplateFiles(nuxtRoot);
|
|
1860
|
+
for (const file of nuxtTemplateFiles) {
|
|
1861
|
+
await writeFileIfChanged(file.path, file.contents);
|
|
1862
|
+
}
|
|
1863
|
+
const nuxtIndexTemplatePath = join(nuxtRoot, "index.template.md");
|
|
1864
|
+
const nuxtIndexTemplate = await pathExists(nuxtIndexTemplatePath) ? await promises.readFile(nuxtIndexTemplatePath, "utf8") : "";
|
|
1865
|
+
const nuxtIndexContent = await renderAutomdTemplate(nuxtIndexTemplate, nuxtRoot);
|
|
1866
|
+
await writeFileIfChanged(join(nuxtRoot, "index.md"), nuxtIndexContent);
|
|
1867
|
+
const vueTemplateFiles = await buildVueTemplateFiles(vueRoot, remoteCacheRoot);
|
|
1868
|
+
for (const file of vueTemplateFiles) {
|
|
1869
|
+
await writeFileIfChanged(file.path, file.contents);
|
|
1870
|
+
}
|
|
1871
|
+
const generatedEntries = [];
|
|
1872
|
+
for (const contribution of contributions) {
|
|
1873
|
+
const includeScripts = contribution.forceIncludeScripts;
|
|
1874
|
+
const destination = createModuleDestination(modulesRoot, contribution);
|
|
1875
|
+
await copySkillTree(contribution.sourceDir, destination, includeScripts);
|
|
1876
|
+
const entryPath = relative(skillRoot, join(destination, "SKILL.md"));
|
|
1877
|
+
generatedEntries.push({
|
|
1878
|
+
packageName: contribution.packageName,
|
|
1879
|
+
version: contribution.version,
|
|
1880
|
+
skillName: contribution.skillName,
|
|
1881
|
+
entryPath,
|
|
1882
|
+
sourceDir: contribution.sourceDir,
|
|
1883
|
+
destination: relative(skillRoot, destination),
|
|
1884
|
+
scriptsIncluded: includeScripts,
|
|
1885
|
+
description: contribution.description,
|
|
1886
|
+
sourceKind: contribution.sourceKind,
|
|
1887
|
+
sourceLabel: getSourceLabel(contribution.sourceKind),
|
|
1888
|
+
sourceRepo: contribution.sourceRepo,
|
|
1889
|
+
sourceRef: contribution.sourceRef,
|
|
1890
|
+
sourcePath: contribution.sourcePath,
|
|
1891
|
+
repoUrl: contribution.repoUrl,
|
|
1892
|
+
docsUrl: contribution.docsUrl,
|
|
1893
|
+
official: contribution.official,
|
|
1894
|
+
trustLevel: getTrustLevel(contribution.official),
|
|
1895
|
+
resolver: contribution.resolver
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
await writeFileIfChanged(join(skillRoot, "SKILL.md"), createSkillEntrypoint(
|
|
1899
|
+
resolvedSkillName,
|
|
1900
|
+
nuxtMetadata,
|
|
1901
|
+
monorepoScopePath,
|
|
1902
|
+
Boolean(options.moduleAuthoring),
|
|
1903
|
+
generatedEntries,
|
|
1904
|
+
skipped
|
|
1905
|
+
));
|
|
1906
|
+
logger.success(`Generated ${resolvedSkillName} skill at ${skillRoot}`);
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
export { module$1 as default };
|