nuxt-skill-hub 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.mjs CHANGED
@@ -1,98 +1,28 @@
1
- import { promises, existsSync, lstatSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join, basename, resolve, dirname, relative, isAbsolute } from 'pathe';
3
1
  import { useLogger, defineNuxtModule } from '@nuxt/kit';
4
2
  import { isCI, isTest } from 'std-env';
5
- import { createConsola } from 'consola';
6
- import { colorize } from 'consola/utils';
7
- import { detectInstalledAgents, expandPath, detectCurrentAgent, getAgentIds, getAgentConfig } from 'unagent/env';
3
+ import { promises, existsSync, lstatSync, writeFileSync, readFileSync } from 'node:fs';
4
+ import { join, basename, resolve, dirname, relative, isAbsolute } from 'pathe';
5
+ import { fileURLToPath } from 'mlly';
8
6
  import { homedir } from 'node:os';
9
7
  import matter from 'gray-matter';
10
- import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
8
+ import { readPackageJSON, findWorkspaceDir, resolvePackageJSON, resolveLockfile } from 'pkg-types';
11
9
  import { transform } from 'automd';
12
- import { fileURLToPath } from 'mlly';
13
10
  import { downloadTemplate } from 'giget';
11
+ import { detectInstalledAgents, expandPath, detectCurrentAgent, getAgentIds, getAgentConfig } from 'unagent/env';
14
12
  import { ofetch } from 'ofetch';
13
+ import { createHash } from 'node:crypto';
14
+ import { hash } from 'ohash';
15
+ import { glob } from 'tinyglobby';
16
+ import { createConsola } from 'consola';
17
+ import { colorize } from 'consola/utils';
15
18
 
16
- const version = "0.0.4";
19
+ const version = "0.0.6";
17
20
  const packageJson = {
18
21
  version: version};
19
22
 
20
23
  const metadata = packageJson;
21
24
  const PACKAGE_VERSION = metadata.version;
22
25
 
23
- function resolveSkillsDir(target, config) {
24
- if (config?.skillsDir) {
25
- return config.skillsDir;
26
- }
27
- if (target === "codex") {
28
- return "skills";
29
- }
30
- return void 0;
31
- }
32
- function normalizeTarget(target) {
33
- return target.trim();
34
- }
35
- function getRawTargetConfig(target) {
36
- return getAgentConfig(target);
37
- }
38
- function getSupportedTargets() {
39
- return getAgentIds().filter((target) => {
40
- const config = getRawTargetConfig(target);
41
- return Boolean(resolveSkillsDir(target, config));
42
- }).sort((a, b) => a.localeCompare(b));
43
- }
44
- function resolveAgentTargetConfig(target) {
45
- const normalized = normalizeTarget(target);
46
- if (!normalized) {
47
- return void 0;
48
- }
49
- const config = getRawTargetConfig(normalized);
50
- const skillsDir = resolveSkillsDir(normalized, config);
51
- if (!config || !skillsDir) {
52
- return void 0;
53
- }
54
- return {
55
- target: normalized,
56
- configDir: expandPath(config.configDir),
57
- skillsDir
58
- };
59
- }
60
- function validateTargets(targets) {
61
- const uniqueTargets = Array.from(new Set(targets.map(normalizeTarget).filter(Boolean)));
62
- const valid = [];
63
- const invalid = [];
64
- for (const target of uniqueTargets) {
65
- const config = getRawTargetConfig(target);
66
- if (!config) {
67
- invalid.push({
68
- target,
69
- reason: "unknown-target"
70
- });
71
- continue;
72
- }
73
- if (!resolveSkillsDir(target, config)) {
74
- invalid.push({
75
- target,
76
- reason: "missing-skills-dir"
77
- });
78
- continue;
79
- }
80
- valid.push(target);
81
- }
82
- return { valid, invalid };
83
- }
84
- function detectCurrentTarget() {
85
- const current = detectCurrentAgent();
86
- if (!current) return void 0;
87
- const skillsDir = resolveSkillsDir(current.id, current.config);
88
- return skillsDir ? current.id : void 0;
89
- }
90
- function detectInstalledTargets() {
91
- return Array.from(new Set(
92
- detectInstalledAgents().filter((agent) => agent.detected === "config" && resolveSkillsDir(agent.id, agent.config)).map((agent) => agent.id)
93
- )).sort((a, b) => a.localeCompare(b));
94
- }
95
-
96
26
  const DEFAULT_NUXT_CONTENT_METADATA = {
97
27
  id: "nuxt-best-practices",
98
28
  version: PACKAGE_VERSION,
@@ -207,7 +137,6 @@ const DEFAULT_NUXT_CONTENT_METADATA = {
207
137
  }
208
138
  ]
209
139
  };
210
- const EMPTY_MODULE_GUIDANCE_MARKDOWN = "_No module skills discovered. Use Nuxt guidance plus official module docs when module-specific guidance is missing._";
211
140
  function yamlString(value) {
212
141
  return JSON.stringify(value);
213
142
  }
@@ -315,107 +244,85 @@ ${lines.join("\n")}
315
244
  function findPack(metadata, id) {
316
245
  return metadata.packs.find((pack) => pack.id === id) || metadata.packs[0];
317
246
  }
318
- function createPackSummary(metadata, packIds) {
319
- return packIds.map((packId) => `\`${findPack(metadata, packId).title}\``).join(", ");
320
- }
321
- function getSkillRenderProfile(includeModuleAuthoring = false) {
322
- if (includeModuleAuthoring) {
323
- return {
324
- includeModuleAuthoring,
325
- packSummaryIds: [
326
- "module-authoring",
327
- "plugins",
328
- "architecture-boundaries",
329
- "nitro-h3-patterns",
330
- "migrations"
331
- ]
332
- };
333
- }
334
- return {
335
- includeModuleAuthoring: false,
336
- packSummaryIds: [
337
- "abstraction-disambiguation",
338
- "page-meta-head-layout",
339
- "error-surfaces-recovery",
340
- "verification-finish",
341
- "data-fetching-ssr"
342
- ]
343
- };
344
- }
345
247
  function packLink(metadata, id) {
346
248
  const pack = findPack(metadata, id);
347
249
  return `[${pack.title}](./references/nuxt/${pack.relativePath})`;
348
250
  }
349
251
  function createRoutingTable(metadata, includeModuleAuthoring = false) {
350
252
  const rows = [
351
- ...includeModuleAuthoring ? [{
352
- symptom: "Writing, refactoring, or publishing a Nuxt module",
353
- packId: "module-authoring",
354
- why: "Start with Nuxt Kit-safe authoring patterns, lifecycle hooks, prefixed public APIs, and skill scope boundaries."
355
- }] : [],
356
- {
357
- symptom: "SSR, initial page load, route params, or hydration-sensitive data",
358
- packId: "data-fetching-ssr",
359
- why: "Prefer `useFetch` or `useAsyncData` over setup-time `$fetch` or `onMounted` fetches."
360
- },
361
- {
362
- symptom: "Page options, route middleware, layout selection, title, meta tags, or OG data",
363
- packId: "page-meta-head-layout",
364
- why: "Separate page behavior from document metadata and layout structure before editing."
365
- },
366
- {
367
- symptom: "A generic Vue fix or raw HTML implementation looks tempting",
368
- packId: "abstraction-disambiguation",
369
- why: "Check whether Nuxt, Nuxt Content, or Nuxt UI already owns the abstraction."
370
- },
371
- {
372
- symptom: "The remaining work is mostly `.vue` components, composables, reactivity, or SFC structure",
373
- link: "[Vue Best Practices](./references/vue/SKILL.md)",
374
- why: "Switch to Vue-specific guidance after the Nuxt ownership and routing decisions are already settled."
375
- },
376
- {
377
- symptom: "Global errors, local fallback UI, `clearError`, `showError`, or recovery flows",
378
- packId: "error-surfaces-recovery",
379
- why: "Nuxt error handling often needs both global and local surfaces to be correct."
380
- },
381
- {
382
- symptom: "Secrets, runtime config, privileged API calls, or server/client boundary confusion",
383
- packId: "architecture-boundaries",
384
- why: "Move sensitive logic server-side first, then pair with config and route rules as needed."
385
- },
386
- {
387
- symptom: "Before finishing a fix that spans multiple files or surfaces",
388
- packId: "verification-finish",
389
- why: "Re-check paired surfaces and verify framework behavior, not only the visible output."
390
- }
253
+ ...includeModuleAuthoring ? [[
254
+ "Writing or refactoring a Nuxt module (`defineNuxtModule`, hooks, public APIs)",
255
+ packLink(metadata, "module-authoring")
256
+ ]] : [],
257
+ [
258
+ "SSR, initial page load, route params, or data fetched for first render",
259
+ packLink(metadata, "data-fetching-ssr")
260
+ ],
261
+ [
262
+ "Hydration warnings, `ClientOnly`, browser-only APIs, time/randomness, or SSR/CSR mismatch",
263
+ packLink(metadata, "hydration-consistency")
264
+ ],
265
+ [
266
+ "Page options, middleware, layout selection, title, meta tags, canonical URLs, or OG data",
267
+ packLink(metadata, "page-meta-head-layout")
268
+ ],
269
+ [
270
+ "`nuxt.config.*`, `runtimeConfig`, env wiring, or public/private config exposure",
271
+ packLink(metadata, "server-routes-runtime-config")
272
+ ],
273
+ [
274
+ "`server/api/**`, `server/routes/**`, `defineEventHandler`, route rules, caching, or Nitro plugins",
275
+ packLink(metadata, "nitro-h3-patterns")
276
+ ],
277
+ [
278
+ "Plugins, injections, app boot logic, or global runtime initialization",
279
+ packLink(metadata, "plugins")
280
+ ],
281
+ [
282
+ "Secrets, privileged API calls, request isolation, or server/client boundary confusion",
283
+ packLink(metadata, "architecture-boundaries")
284
+ ],
285
+ [
286
+ "Performance regressions, rendering strategy, lazy loading, or payload/bundle cost",
287
+ packLink(metadata, "performance-rendering")
288
+ ],
289
+ [
290
+ "Upgrades, deprecations, compatibility fixes, or version-boundary work",
291
+ packLink(metadata, "migrations")
292
+ ],
293
+ [
294
+ "A generic Vue or raw HTML fix looks plausible",
295
+ packLink(metadata, "abstraction-disambiguation")
296
+ ],
297
+ [
298
+ "The remaining work is mostly components, composables, reactivity, props/emits, or SFC structure",
299
+ "[Vue Best Practices](./references/vue/SKILL.md)"
300
+ ],
301
+ [
302
+ "Global/local errors, `clearError`, `showError`, or recovery flows",
303
+ packLink(metadata, "error-surfaces-recovery")
304
+ ]
391
305
  ];
392
306
  return [
393
- "| Task shape or symptom | Load first | Why |",
394
- "| --- | --- | --- |",
395
- ...rows.map((row) => `| ${row.symptom} | ${"link" in row ? row.link : packLink(metadata, row.packId)} | ${row.why} |`)
396
- ].join("\n");
397
- }
398
- function createNuxtPackTable(metadata) {
399
- const rows = metadata.packs.map(
400
- (pack) => `| [${pack.title}](./references/nuxt/${pack.relativePath}) | ${pack.focus} | ${pack.triggers.join("<br>")} |`
401
- );
402
- return [
403
- "| Pack | Focus | Typical triggers |",
404
- "| --- | --- | --- |",
405
- ...rows
307
+ "| Task shape or symptom | Open first |",
308
+ "| --- | --- |",
309
+ ...rows.map(([symptom, target]) => `| ${symptom} | ${target} |`)
406
310
  ].join("\n");
407
311
  }
408
- function createVueGuidanceSection() {
409
- return `## Vue guidance
410
-
411
- 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.
412
-
413
- Start with these must-read Vue references:
414
- - [reactivity](./references/vue/references/reactivity.md)
415
- - [sfc](./references/vue/references/sfc.md)
416
- - [component-data-flow](./references/vue/references/component-data-flow.md)
417
- - [composables](./references/vue/references/composables.md)
418
- `;
312
+ function createRoutingExamples(metadata, includeModuleAuthoring = false) {
313
+ const lines = [
314
+ `- "This page fetches twice on first load" \u2192 ${packLink(metadata, "data-fetching-ssr")}`,
315
+ `- "Why do I get a hydration mismatch from Date.now()?" \u2192 ${packLink(metadata, "hydration-consistency")}`,
316
+ `- "Change canonical URL and OG tags" \u2192 ${packLink(metadata, "page-meta-head-layout")}`,
317
+ `- "Expose only the public runtimeConfig key" \u2192 ${packLink(metadata, "server-routes-runtime-config")}`,
318
+ `- "This server/api handler cache or route rule is wrong" \u2192 ${packLink(metadata, "nitro-h3-patterns")}`,
319
+ `- "This modal/table/dropdown was hand-built in raw HTML" \u2192 ${packLink(metadata, "abstraction-disambiguation")}`,
320
+ `- "\`clearError\` is not behaving as expected" \u2192 ${packLink(metadata, "error-surfaces-recovery")}`,
321
+ `- "Should this secret live in runtime config or client code?" \u2192 ${packLink(metadata, "architecture-boundaries")}`,
322
+ ...includeModuleAuthoring ? [`- "Add a Nuxt module option, hook, or runtime extension" \u2192 ${packLink(metadata, "module-authoring")}`] : []
323
+ ];
324
+ return `## Routing examples
325
+ ${lines.join("\n")}`;
419
326
  }
420
327
  function createSkillMapSections(metadata, entries, skipped = [], includeModuleAuthoring = false) {
421
328
  const includeModuleSections = hasModuleGuidance(entries, skipped);
@@ -426,137 +333,85 @@ function createSkillMapSections(metadata, entries, skipped = [], includeModuleAu
426
333
  renderModuleGroup("Metadata-routed skills", grouped.metadataRouted, "./references/modules/"),
427
334
  renderSkippedEntries(skipped)
428
335
  ].filter(Boolean);
429
- const moduleGuideContent = moduleSections.join("\n");
430
- const audienceGuide = includeModuleAuthoring ? `
431
- ## Module author focus
432
-
433
- This skill includes extra guidance for Nuxt module authors on top of the default app-oriented Nuxt packs.
434
- 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.
435
- ` : "";
436
- const moduleWorkflowSteps = includeModuleSections ? `
437
- 5. If an installed module is involved, open its entry under \`references/modules\`.
438
- 6. Copied skills go straight to their \`SKILL.md\`; metadata-routed modules only expose docs and source links.` : "";
439
- const moduleGuidesSection = includeModuleSections ? `
440
- ## Module guides
336
+ const moduleGuidesSection = includeModuleSections ? `## Module guides
337
+ Use a module entry only when an installed module owns the surface. Module guidance is delta-only and should not replace broad Nuxt rules outside that module.
441
338
 
442
- 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.
339
+ ${moduleSections.join("\n")}` : "";
340
+ return [
341
+ "## Routing",
342
+ "Load one matching guide first. Open more guides only when the first guide points you there or the task clearly crosses boundaries.",
343
+ createRoutingTable(metadata, includeModuleAuthoring),
344
+ createRoutingExamples(metadata, includeModuleAuthoring),
345
+ moduleGuidesSection
346
+ ].filter(Boolean).join("\n\n");
347
+ }
348
+ function createSkillEntrypoint(skillName, metadata, monorepoScopePath, includeModuleAuthoring = false, entries = [], skipped = []) {
349
+ const description = "Routes Nuxt work to the smallest relevant guide. Use for `useFetch`/`useAsyncData`, SSR, hydration mismatches, `definePageMeta`, `useHead`/`useSeoMeta`, `runtimeConfig`, Nitro/h3 server routes, plugins, Nuxt UI, Nuxt Content, or Nuxt module authoring.";
350
+ const monorepoScopeSection = monorepoScopePath ? `## Monorepo scope
351
+ This skill applies only to \`${monorepoScopePath}\`. Treat files and tasks outside that subtree as out of scope unless the user explicitly redirects you there.` : "";
352
+ return `---
353
+ name: ${yamlString(skillName)}
354
+ description: ${yamlString(description)}
355
+ ---
443
356
 
444
- ${moduleGuideContent}
445
- ` : `
446
- ## Module guides
357
+ # Nuxt Skill Router
447
358
 
448
- ${EMPTY_MODULE_GUIDANCE_MARKDOWN}
449
- `;
450
- return `## How to use this skill map
451
- 1. Explore the current surface first: page, layout, component, server handler, content collection, or module-owned file.
452
- 2. Load the first matching Nuxt pack from the routing table below.
453
- 3. If the remaining work is mainly Vue component or composable implementation, switch to [Vue Best Practices](./references/vue/SKILL.md).
454
- 4. Open deeper Nuxt packs only when the first pack points you there.
455
- ${moduleWorkflowSteps}
456
- ${audienceGuide}
359
+ Use this skill to make the Nuxt ownership decision first, then route to the smallest relevant Nuxt, Vue, or module guide.
360
+ ${monorepoScopeSection ? `
361
+ ${monorepoScopeSection}
362
+ ` : "\n"}## Loading rules
363
+ 1. Repository-global instructions and required workflows win first.
364
+ 2. Inspect the exact owned surface first: page, layout, component, server handler, collection, config file, or module-owned file.
365
+ 3. Load one matching guide from the routing table below.
366
+ 4. Switch to [Vue Best Practices](./references/vue/SKILL.md) only when Nuxt no longer owns the abstraction.
367
+ 5. Use module entries only inside module-owned APIs, config, runtime behavior, and files.
368
+ 6. Open additional guides only when the first guide points you there or the task clearly crosses boundaries.
457
369
 
458
- ## Common forks in the road
370
+ ${createSkillMapSections(metadata, entries, skipped, includeModuleAuthoring)}
459
371
 
460
- ${createRoutingTable(metadata, includeModuleAuthoring)}
372
+ ## Finish
373
+ For multi-surface changes or final verification, open ${packLink(metadata, "verification-finish")}.
461
374
 
462
- ${createVueGuidanceSection()}
375
+ - Verify paired surfaces when relevant: page/head, server/client, global/local.
376
+ - Verify framework behavior, not only visible output.
377
+ - Keep module guidance scoped to the owning module.
463
378
 
464
- ## All Nuxt packs
379
+ ---
465
380
 
466
- ${createNuxtPackTable(metadata)}
467
- ${moduleGuidesSection}
381
+ _Generated by nuxt-skill-hub. Do not edit this file manually._
468
382
  `;
469
383
  }
470
- function createSkillEntrypoint(skillName, metadata, monorepoScopePath, includeModuleAuthoring = false, entries = [], skipped = []) {
471
- const profile = getSkillRenderProfile(includeModuleAuthoring);
472
- const includeModuleSections = hasModuleGuidance(entries, skipped);
473
- 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.";
474
- const monorepoScopeSection = monorepoScopePath ? `
475
- ## Monorepo Scope
476
- This skill applies only to the \`${monorepoScopePath}\` subtree of this monorepo.
477
- Treat files and tasks outside \`${monorepoScopePath}\` as out of scope unless the user explicitly redirects you there.
478
- ` : "";
479
- const audienceSection = profile.includeModuleAuthoring ? `
480
-
481
- ## Module Author Focus
482
- This skill keeps the default Nuxt app guidance and adds an authoring layer for repositories that build Nuxt modules.
483
- 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.
484
- ` : "";
485
- const activationFlow = `1. Explore the project first: inspect the real page, component, route, server handler, collection, or module surface you are changing.
486
- 2. Use the routing sections in this file and load the smallest matching Nuxt pack.
487
- 3. If the remaining work is mainly Vue component, composable, reactivity, or SFC work, open [Vue Best Practices](./references/vue/SKILL.md).
488
- 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 ? `
489
- 5. If an installed module owns the problem, open its entry under [references/modules](./references/modules).
490
- 6. Apply module guidance as delta-only rules inside that module's APIs, config, runtime behavior, and owned files.` : ""}`;
491
- 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.
492
- - If the module exposes routes, composables, components, or config, prefix public surfaces with module identity before shipping generic names.
493
- - If the implementation relies on undocumented Nuxt internals, confirm there is not a public \`@nuxt/kit\` API or documented hook first.
494
- - If the task adds or edits a bundled module skill, keep it strictly scoped to the module's APIs, integration points, and caveats.
495
- - 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.
496
- - If the work is version-sensitive, check compatibility constraints and migration boundaries before expanding the module surface.
497
- - If the task touches SSR, initial page load, or route-driven data, prefer \`useFetch\` or \`useAsyncData\` before \`onMounted\` plus \`$fetch\`.
498
- - If the task changes page options, layout selection, route middleware, or page-level behavior, check \`definePageMeta\` before adding ad hoc wiring.
499
- - If the task changes title, meta tags, canonical URLs, or OG data, check \`useHead\` or \`useSeoMeta\` before page-meta or template markup.
500
- - 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.
501
- - 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.
502
- - 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.
503
- - 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\`.
504
- - If the fix touches errors, fallback UI, or recovery flow, check both global and local surfaces before concluding the work is complete.
505
- - 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 ? `
506
- - 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\`.
507
- - If the task changes page options, layout selection, route middleware, or page-level behavior, check \`definePageMeta\` before adding ad hoc wiring.
508
- - If the task changes title, meta tags, canonical URLs, or OG data, check \`useHead\` or \`useSeoMeta\` before page-meta or template markup.
509
- - 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.
510
- - 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.
511
- - 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.
512
- - 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\`.
513
- - If the fix touches errors, fallback UI, or recovery flow, check both global and local surfaces before concluding the work is complete.
514
- - 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 ? `
515
- - 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.` : ""}`;
516
- const beforeCompletion = profile.includeModuleAuthoring ? `- Did I start from Nuxt Kit and documented lifecycle hooks before reaching for private internals?
517
- - Did I keep public APIs collision-resistant and module-scoped?
518
- - Did I keep module skill guidance as a delta on top of Nuxt guidance instead of restating framework-global rules?
519
- - Did I verify compatibility, install/setup cost, and cross-boundary behavior before concluding the work is complete?
520
- - 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?
521
- - Did I check whether the fix needs a second surface such as global or local, or server or client?
522
- - Did I choose the right concept pair: page meta vs head, data collection vs page collection, component primitive vs custom markup?
523
- - Did I verify the framework behavior that matters, not just the visible output?`;
384
+ function createStableSkillWrapper(skillName, generatedEntryPath, generatedRootPath, generationMode) {
385
+ const description = "Stable entrypoint for generated Nuxt guidance in this repository. Use for Nuxt SSR, hydration, pages, `runtimeConfig`, Nitro/h3, plugins, Nuxt UI, Nuxt Content, or Nuxt module work. If generated content is missing, regenerate it with `nuxt prepare`.";
386
+ const recoveryInstructions = generationMode === "manual" ? [
387
+ "Automatic skill generation is currently disabled by `skillHub.generationMode: 'manual'`.",
388
+ "Ask the user to switch `skillHub.generationMode` to `'prepare'`, then run `nuxt prepare`."
389
+ ].join("\n") : [
390
+ "Run `nuxt prepare` from this project before continuing.",
391
+ "That regenerates the Nuxt build directory, refreshes types, and writes the generated Nuxt skill content."
392
+ ].join("\n");
524
393
  return `---
525
394
  name: ${yamlString(skillName)}
526
395
  description: ${yamlString(description)}
527
396
  ---
528
397
 
529
- # Nuxt Skill Index
530
-
531
- This file keeps the highest-frequency Nuxt decisions in context.
532
- Use it to avoid generic Vue fallbacks, then route into the right Nuxt pack, Vue guidance, or module delta skill.
533
- ${monorepoScopeSection}
534
- ${audienceSection}
535
-
536
- ## Activation Flow
537
- ${activationFlow}
538
-
539
- ## High-Frequency Nuxt Decisions
540
- ${frequencyBullets}
541
-
542
- ## Precedence
543
- - Repository-global instructions and required workflows win first.
544
- - This file keeps the common Nuxt forks in context.
545
- - Nuxt packs provide deeper framework guidance.
546
- - Vue guidance covers component, composable, and SFC patterns after the Nuxt decision is settled.
547
- ${includeModuleSections ? "- Module entries add delta-only guidance inside explicit module scope." : ""}
548
-
549
- ${createSkillMapSections(metadata, entries, skipped, includeModuleAuthoring)}
398
+ # Nuxt Skill Wrapper
550
399
 
551
- ## Before Completion
552
- ${beforeCompletion}
400
+ This is the stable entrypoint for Nuxt guidance in this repository.
401
+ The full generated skill tree lives in the Nuxt build directory so the checked-in skill surface stays small.
553
402
 
554
- ## Primary Packs
555
- Start with ${createPackSummary(metadata, profile.packSummaryIds)} when the task matches one of those common routes.
403
+ ## Generated content
404
+ - Entry: [${generatedEntryPath}](${generatedEntryPath})
405
+ - Root: [${generatedRootPath}](${generatedRootPath})
556
406
 
557
- ---
407
+ ## Required workflow
408
+ 1. Check whether the generated entry exists.
409
+ 2. If it exists, open it and follow that generated skill instead of extending this wrapper.
410
+ 3. If it does not exist, ${recoveryInstructions}
558
411
 
559
- _Generated by nuxt-skill-hub. Do not edit this file manually._
412
+ ## Notes
413
+ - Generated content is ephemeral and may be refreshed by Nuxt.
414
+ - This wrapper is the stable file that should remain in version control.
560
415
  `;
561
416
  }
562
417
  function createCompactMetadataRouterContent(entry) {
@@ -630,6 +485,79 @@ async function loadVueSkillFiles(cacheRoot) {
630
485
  return loadVueSkillFilesFromDir(dir);
631
486
  }
632
487
 
488
+ function resolveSkillsDir(target, config) {
489
+ if (config?.skillsDir) {
490
+ return config.skillsDir;
491
+ }
492
+ if (target === "codex") {
493
+ return "skills";
494
+ }
495
+ return void 0;
496
+ }
497
+ function normalizeTarget(target) {
498
+ return target.trim();
499
+ }
500
+ function getRawTargetConfig(target) {
501
+ return getAgentConfig(target);
502
+ }
503
+ function getSupportedTargets() {
504
+ return getAgentIds().filter((target) => {
505
+ const config = getRawTargetConfig(target);
506
+ return Boolean(resolveSkillsDir(target, config));
507
+ }).sort((a, b) => a.localeCompare(b));
508
+ }
509
+ function resolveAgentTargetConfig(target) {
510
+ const normalized = normalizeTarget(target);
511
+ if (!normalized) {
512
+ return void 0;
513
+ }
514
+ const config = getRawTargetConfig(normalized);
515
+ const skillsDir = resolveSkillsDir(normalized, config);
516
+ if (!config || !skillsDir) {
517
+ return void 0;
518
+ }
519
+ return {
520
+ target: normalized,
521
+ configDir: expandPath(config.configDir),
522
+ skillsDir
523
+ };
524
+ }
525
+ function validateTargets(targets) {
526
+ const uniqueTargets = Array.from(new Set(targets.map(normalizeTarget).filter(Boolean)));
527
+ const valid = [];
528
+ const invalid = [];
529
+ for (const target of uniqueTargets) {
530
+ const config = getRawTargetConfig(target);
531
+ if (!config) {
532
+ invalid.push({
533
+ target,
534
+ reason: "unknown-target"
535
+ });
536
+ continue;
537
+ }
538
+ if (!resolveSkillsDir(target, config)) {
539
+ invalid.push({
540
+ target,
541
+ reason: "missing-skills-dir"
542
+ });
543
+ continue;
544
+ }
545
+ valid.push(target);
546
+ }
547
+ return { valid, invalid };
548
+ }
549
+ function detectCurrentTarget() {
550
+ const current = detectCurrentAgent();
551
+ if (!current) return void 0;
552
+ const skillsDir = resolveSkillsDir(current.id, current.config);
553
+ return skillsDir ? current.id : void 0;
554
+ }
555
+ function detectInstalledTargets() {
556
+ return Array.from(new Set(
557
+ detectInstalledAgents().filter((agent) => agent.detected === "config" && resolveSkillsDir(agent.id, agent.config)).map((agent) => agent.id)
558
+ )).sort((a, b) => a.localeCompare(b));
559
+ }
560
+
633
561
  const MANAGED_HINT_START = "<!-- nuxt-skill-hub:start -->";
634
562
  const MANAGED_HINT_END = "<!-- nuxt-skill-hub:end -->";
635
563
  const SKILL_NAME_MAX_LENGTH = 64;
@@ -683,11 +611,19 @@ async function hasWorkspacePackageConfig(path) {
683
611
  }
684
612
  async function resolveExportRoot(rootDir) {
685
613
  const appRoot = resolve(rootDir);
614
+ const workspaceDir = await findWorkspaceDir(appRoot).catch(() => null);
615
+ if (!workspaceDir) {
616
+ return appRoot;
617
+ }
618
+ const resolvedWorkspaceDir = resolve(workspaceDir);
686
619
  let current = appRoot;
687
620
  while (true) {
688
621
  if (await pathExists(join(current, "pnpm-workspace.yaml")) || await hasWorkspacePackageConfig(current)) {
689
622
  return current;
690
623
  }
624
+ if (current === resolvedWorkspaceDir) {
625
+ return appRoot;
626
+ }
691
627
  const parent = dirname(current);
692
628
  if (parent === current) {
693
629
  return appRoot;
@@ -1104,217 +1040,20 @@ function formatConflictWarning(conflict) {
1104
1040
  return `Found ${conflict.skillNames.length} standalone skill(s) that may conflict with nuxt-skill-hub: ${conflict.skillNames.join(", ")} (global: ${conflict.globalDir}). Remove with: npx skills remove --global`;
1105
1041
  }
1106
1042
 
1107
- function isCancelled(value) {
1108
- return value === null || typeof value === "symbol";
1109
- }
1110
- async function runInstallWizard(nuxt) {
1111
- const logger = useLogger("nuxt-skill-hub");
1112
- if (process.env.CI || !process.stdout.isTTY) {
1113
- logger.info("Non-interactive environment detected, skipping install wizard.");
1114
- return;
1115
- }
1116
- const consola = createConsola();
1117
- const rootDir = nuxt.options.rootDir;
1118
- const exportRoot = await resolveExportRoot(rootDir);
1119
- const skillName = await deriveSkillName(rootDir);
1120
- const pendingWrites = [];
1121
- consola.box(
1122
- `nuxt-skill-hub \u2014 First-time setup
1123
- Let's configure AI agent skills for your Nuxt project.`
1124
- );
1125
- const currentTarget = detectCurrentTarget();
1126
- let selectedTargets;
1127
- if (currentTarget) {
1128
- consola.info(`Running inside ${currentTarget}, auto-selecting it.`);
1129
- selectedTargets = [currentTarget];
1130
- } else {
1131
- const detectedTargets = detectInstalledTargets();
1132
- const allTargets = getSupportedTargets();
1133
- if (!detectedTargets.length && !allTargets.length) {
1134
- consola.warn("No AI agents detected. Skills will be generated when an agent is detected.");
1135
- consola.info("Supported agents are sourced from unagent.");
1136
- return;
1137
- }
1138
- consola.info(`Detected agents: ${detectedTargets.length ? detectedTargets.join(", ") : "none"}`);
1139
- selectedTargets = detectedTargets;
1140
- if (detectedTargets.length > 1) {
1141
- const keepAll = await consola.prompt("Generate skills for all detected agents?", {
1142
- type: "confirm",
1143
- initial: true,
1144
- cancel: "null"
1145
- });
1146
- if (isCancelled(keepAll)) {
1147
- consola.info("Setup cancelled.");
1148
- return;
1149
- }
1150
- if (!keepAll) {
1151
- const chosen = await consola.prompt("Select agents to generate skills for:", {
1152
- type: "multiselect",
1153
- options: detectedTargets.map((t) => ({ label: t, value: t })),
1154
- initial: detectedTargets,
1155
- required: true,
1156
- cancel: "null"
1157
- });
1158
- if (isCancelled(chosen)) {
1159
- consola.info("Setup cancelled.");
1160
- return;
1161
- }
1162
- selectedTargets = chosen.map((c) => c.value);
1163
- }
1164
- }
1165
- }
1166
- const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
1167
- const uniqueSpecifiers = Array.from(new Set(moduleSpecifiers));
1168
- const installedModulePackages = [];
1169
- for (const specifier of uniqueSpecifiers) {
1170
- const pkg = await discoverInstalledPackageFromSpecifier(specifier, rootDir);
1171
- if (!pkg || pkg.packageName === "nuxt-skill-hub")
1172
- continue;
1173
- installedModulePackages.push(pkg.packageName);
1174
- }
1175
- if (installedModulePackages.length) {
1176
- consola.info(`Detected ${installedModulePackages.length} installed module package(s):`);
1177
- for (const packageName of installedModulePackages) {
1178
- consola.log(` ${packageName}`);
1179
- }
1180
- consola.info("Module skills and metadata routers are resolved automatically at build time.");
1181
- } else {
1182
- consola.info("No installed module entries were discovered.");
1183
- }
1184
- for (const conflict of detectConflictingSkills(selectedTargets, skillName)) {
1185
- consola.warn(formatConflictWarning(conflict));
1186
- }
1187
- const gitignorePath = join(exportRoot, ".gitignore");
1188
- const gitignoreExists = existsSync(gitignorePath);
1189
- const currentGitignore = gitignoreExists ? readFileSync(gitignorePath, "utf8") : "";
1190
- const missingPatterns = [];
1191
- for (const target of selectedTargets) {
1192
- const { skillRoot } = getTargetSkillRoot(exportRoot, target, skillName);
1193
- const pattern = relative(exportRoot, skillRoot).replace(/\/?$/, "/");
1194
- if (!currentGitignore.includes(pattern)) {
1195
- missingPatterns.push(pattern);
1196
- }
1197
- }
1198
- if (missingPatterns.length) {
1199
- const addGitignore = await consola.prompt(
1200
- `Add generated skill directories to .gitignore?
1201
- ${missingPatterns.map((p) => ` ${p}`).join("\n")}`,
1202
- { type: "confirm", initial: true, cancel: "null" }
1203
- );
1204
- if (isCancelled(addGitignore)) {
1205
- consola.info("Setup cancelled.");
1206
- return;
1207
- }
1208
- if (addGitignore) {
1209
- const block = `
1210
- # nuxt-skill-hub (generated skills)
1211
- ${missingPatterns.join("\n")}
1212
- `;
1213
- const newContent = currentGitignore.trimEnd() + "\n" + block;
1214
- pendingWrites.push({
1215
- file: ".gitignore",
1216
- absPath: gitignorePath,
1217
- preview: missingPatterns.map((p) => `+ ${p}`).join("\n"),
1218
- content: newContent,
1219
- action: gitignoreExists ? "modify" : "create"
1220
- });
1221
- }
1222
- }
1223
- const hintBlock = `${MANAGED_HINT_START}
1224
- Use the \`${skillName}\` skill as the first entrypoint for Nuxt tasks in this repository.
1225
- ${MANAGED_HINT_END}`;
1226
- const hintFileChoice = await consola.prompt("Add a skill hint to help agents discover the skill?", {
1227
- type: "select",
1228
- options: [
1229
- { label: "CLAUDE.md", value: "claude", hint: "Claude Code project instructions" },
1230
- { label: "AGENTS.md", value: "agents", hint: "Universal agent instructions" },
1231
- { label: "Both", value: "both" },
1232
- { label: "Skip", value: "skip" }
1233
- ],
1234
- cancel: "null"
1235
- });
1236
- if (isCancelled(hintFileChoice)) {
1237
- consola.info("Setup cancelled.");
1238
- return;
1239
- }
1240
- const addToFile = async (filename) => {
1241
- const filePath = join(exportRoot, filename);
1242
- const exists = await pathExists(filePath);
1243
- const current = exists ? readFileSync(filePath, "utf8") : "";
1244
- const regex = new RegExp(`${MANAGED_HINT_START}[\\s\\S]*?${MANAGED_HINT_END}`);
1245
- let newContent;
1246
- if (regex.test(current)) {
1247
- newContent = current.replace(regex, hintBlock);
1248
- } else if (current) {
1249
- newContent = `${current.trimEnd()}
1250
-
1251
- ${hintBlock}
1252
- `;
1253
- } else {
1254
- newContent = `${hintBlock}
1255
- `;
1256
- }
1257
- pendingWrites.push({
1258
- file: filename,
1259
- absPath: filePath,
1260
- preview: `+ Use the \`${skillName}\` skill as the first entrypoint for Nuxt tasks in this repository.`,
1261
- content: newContent,
1262
- action: exists ? "modify" : "create"
1263
- });
1264
- };
1265
- if (hintFileChoice === "claude" || hintFileChoice === "both") {
1266
- await addToFile("CLAUDE.md");
1267
- }
1268
- if (hintFileChoice === "agents" || hintFileChoice === "both") {
1269
- await addToFile("AGENTS.md");
1270
- }
1271
- if (!pendingWrites.length) {
1272
- consola.success("No file changes needed. Setup complete!");
1273
- consola.info("Run `nuxt dev` or `nuxt prepare` to generate skills.");
1274
- return;
1275
- }
1276
- consola.log("");
1277
- consola.info("Planned changes:");
1278
- for (const write of pendingWrites) {
1279
- consola.box({ title: write.file, message: colorize("green", write.preview), style: { borderColor: "green" } });
1280
- }
1281
- const confirm = await consola.prompt("Apply these changes?", {
1282
- type: "confirm",
1283
- initial: true,
1284
- cancel: "null"
1285
- });
1286
- if (isCancelled(confirm) || !confirm) {
1287
- consola.info("Setup cancelled. No files were modified.");
1288
- return;
1289
- }
1290
- for (const write of pendingWrites) {
1291
- writeFileSync(write.absPath, write.content, "utf8");
1292
- const verb = write.action === "create" ? "Created" : "Updated";
1293
- consola.success(`${verb} ${write.file}`);
1294
- }
1295
- consola.log("");
1296
- consola.box(
1297
- `Setup complete!
1298
-
1299
- Run \`nuxt dev\` or \`nuxt prepare\` to generate skills.
1300
- Configure \`skillHub.skillName\` or \`skillHub.targets\` in nuxt.config.ts if needed.`
1301
- );
1302
- }
1303
-
1304
- const GITHUB_OVERRIDES = [
1305
- {
1306
- packageName: "@nuxt/ui",
1307
- repo: "nuxt/ui",
1308
- ref: "v4",
1309
- path: "skills/nuxt-ui",
1310
- skillName: "nuxt-ui"
1311
- },
1312
- {
1313
- packageName: "@vueuse/core",
1314
- repo: "vueuse/vueuse",
1315
- ref: "main",
1316
- path: "skills/vueuse-functions",
1317
- skillName: "vueuse-functions"
1043
+ const GITHUB_OVERRIDES = [
1044
+ {
1045
+ packageName: "@nuxt/ui",
1046
+ repo: "nuxt/ui",
1047
+ ref: "v4",
1048
+ path: "skills/nuxt-ui",
1049
+ skillName: "nuxt-ui"
1050
+ },
1051
+ {
1052
+ packageName: "@vueuse/core",
1053
+ repo: "vueuse/vueuse",
1054
+ ref: "main",
1055
+ path: "skills/vueuse-functions",
1056
+ skillName: "vueuse-functions"
1318
1057
  }
1319
1058
  ];
1320
1059
  function findGitHubOverride(packageName) {
@@ -1699,11 +1438,198 @@ async function resolveRemoteContributionsForPackage(packageInfo, options) {
1699
1438
  };
1700
1439
  }
1701
1440
 
1702
- const GITHUB_LOOKUP_TIMEOUT_MS = 1500;
1441
+ const DEFAULT_SKILL_HUB_GENERATION_MODE = "prepare";
1442
+ function normalizeGenerationMode(value) {
1443
+ return value === "manual" ? "manual" : DEFAULT_SKILL_HUB_GENERATION_MODE;
1444
+ }
1445
+
1446
+ async function resolveLockfileInfo(exportRoot) {
1447
+ let lockfilePath;
1448
+ try {
1449
+ lockfilePath = await resolveLockfile(exportRoot);
1450
+ } catch {
1451
+ return null;
1452
+ }
1453
+ const contents = await promises.readFile(lockfilePath);
1454
+ const hash = createHash("sha256").update(contents).digest("hex");
1455
+ return {
1456
+ path: relative(exportRoot, lockfilePath).replaceAll("\\", "/"),
1457
+ hash
1458
+ };
1459
+ }
1460
+ async function createGenerationFingerprint(input) {
1461
+ const rootPackageHash = await hashFileIfExists(join(input.rootDir, "package.json"));
1462
+ const lockfile = await resolveLockfileInfo(input.exportRoot);
1463
+ const modules = [...input.installedPackages].filter((pkg) => pkg.packageName !== "nuxt-skill-hub").map((pkg) => ({ name: pkg.packageName, version: pkg.version || null })).sort((a, b) => a.name.localeCompare(b.name));
1464
+ const localSources = [...input.localSources].sort((a, b) => {
1465
+ const left = `${a.packageName}::${a.skillName}::${a.sourceDir}`;
1466
+ const right = `${b.packageName}::${b.skillName}::${b.sourceDir}`;
1467
+ return left.localeCompare(right);
1468
+ });
1469
+ return hash({
1470
+ packageVersion: input.packageVersion,
1471
+ skillName: input.skillName,
1472
+ rootDir: input.rootDir,
1473
+ buildDir: input.buildDir,
1474
+ rootPackageHash,
1475
+ lockfile,
1476
+ modules,
1477
+ localSources,
1478
+ skillHub: {
1479
+ skillName: input.options.skillName?.trim() || null,
1480
+ targets: [...input.options.targets || []].sort(),
1481
+ moduleAuthoring: Boolean(input.options.moduleAuthoring),
1482
+ generationMode: normalizeGenerationMode(input.options.generationMode)
1483
+ }
1484
+ });
1485
+ }
1486
+ async function hashFileIfExists(path) {
1487
+ try {
1488
+ const contents = await promises.readFile(path);
1489
+ return createHash("sha256").update(contents).digest("hex");
1490
+ } catch {
1491
+ return null;
1492
+ }
1493
+ }
1494
+
1703
1495
  async function resolveManualContribution(rootDir, contribution) {
1704
1496
  const sourceDir = isAbsolute(contribution.sourceDir) ? contribution.sourceDir : resolve(rootDir, contribution.sourceDir);
1705
1497
  return normalizeContribution(contribution, sourceDir, sourceDir);
1706
1498
  }
1499
+ function collectWorkspaceDiscoverySources(_rootDir, discoveries) {
1500
+ const contributions = [];
1501
+ for (const discovery of discoveries) {
1502
+ if (!isLocalDiscoveryRoot(discovery.packageRoot)) {
1503
+ continue;
1504
+ }
1505
+ for (const skill of discovery.skills) {
1506
+ const sourceDir = resolve(discovery.packageRoot, skill.path);
1507
+ contributions.push(normalizeContribution({
1508
+ packageName: discovery.packageName,
1509
+ version: discovery.version,
1510
+ skillName: skill.name
1511
+ }, sourceDir, discovery.packageRoot));
1512
+ }
1513
+ }
1514
+ return sortAndDedupeContributions(contributions);
1515
+ }
1516
+ function isLocalDiscoveryRoot(packageRoot) {
1517
+ const normalizedRoot = resolve(packageRoot).replaceAll("\\", "/");
1518
+ return !/(?:^|\/)node_modules(?:\/|$)/.test(normalizedRoot);
1519
+ }
1520
+ async function createLocalSourceFingerprints(contributions) {
1521
+ const fingerprints = await Promise.all(contributions.map(async (contribution) => ({
1522
+ packageName: contribution.packageName,
1523
+ skillName: contribution.skillName,
1524
+ sourceDir: resolve(contribution.sourceDir).replaceAll("\\", "/"),
1525
+ sourceRoot: resolve(contribution.sourceRoot).replaceAll("\\", "/"),
1526
+ sourceKind: contribution.sourceKind,
1527
+ forceIncludeScripts: contribution.forceIncludeScripts,
1528
+ hash: await hashDirectoryTreeIfExists(contribution.sourceDir)
1529
+ })));
1530
+ return fingerprints.sort((a, b) => {
1531
+ const left = `${a.packageName}::${a.skillName}::${a.sourceDir}`;
1532
+ const right = `${b.packageName}::${b.skillName}::${b.sourceDir}`;
1533
+ return left.localeCompare(right);
1534
+ });
1535
+ }
1536
+ async function hashDirectoryTreeIfExists(rootPath) {
1537
+ let rootStat;
1538
+ try {
1539
+ rootStat = await promises.lstat(rootPath);
1540
+ } catch {
1541
+ return null;
1542
+ }
1543
+ if (!rootStat.isDirectory()) {
1544
+ return null;
1545
+ }
1546
+ const hash = createHash("sha256");
1547
+ hash.update("dir:.\n");
1548
+ const entries = await glob("**/*", {
1549
+ cwd: rootPath,
1550
+ dot: true,
1551
+ expandDirectories: false,
1552
+ followSymbolicLinks: false,
1553
+ onlyFiles: false
1554
+ });
1555
+ entries.sort((a, b) => a.localeCompare(b));
1556
+ for (const entry of entries) {
1557
+ const currentPath = join(rootPath, entry);
1558
+ const stat = await promises.lstat(currentPath);
1559
+ const relativePath = relative(rootPath, currentPath).replaceAll("\\", "/");
1560
+ if (stat.isSymbolicLink()) {
1561
+ hash.update(`symlink:${relativePath}:`);
1562
+ hash.update(await promises.readlink(currentPath));
1563
+ hash.update("\n");
1564
+ continue;
1565
+ }
1566
+ if (stat.isDirectory()) {
1567
+ hash.update(`dir:${relativePath}
1568
+ `);
1569
+ continue;
1570
+ }
1571
+ if (stat.isFile()) {
1572
+ hash.update(`file:${relativePath}:`);
1573
+ hash.update(await promises.readFile(currentPath));
1574
+ hash.update("\n");
1575
+ continue;
1576
+ }
1577
+ hash.update(`other:${relativePath}:${stat.mode}
1578
+ `);
1579
+ }
1580
+ return hash.digest("hex");
1581
+ }
1582
+
1583
+ const GENERATION_STATE_FILE = ".state.json";
1584
+ function getGenerationStatePath(generatedSkillRoot) {
1585
+ return join(generatedSkillRoot, GENERATION_STATE_FILE);
1586
+ }
1587
+ async function readGenerationState(generatedSkillRoot) {
1588
+ const statePath = getGenerationStatePath(generatedSkillRoot);
1589
+ if (!await pathExists(statePath)) {
1590
+ return null;
1591
+ }
1592
+ try {
1593
+ return JSON.parse(await promises.readFile(statePath, "utf8"));
1594
+ } catch {
1595
+ return null;
1596
+ }
1597
+ }
1598
+ async function writeGenerationState(generatedSkillRoot, fingerprint) {
1599
+ const state = {
1600
+ fingerprint,
1601
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1602
+ packageVersion: PACKAGE_VERSION
1603
+ };
1604
+ await writeFileIfChanged(getGenerationStatePath(generatedSkillRoot), `${JSON.stringify(state, null, 2)}
1605
+ `);
1606
+ }
1607
+ async function isGeneratedSkillFresh(generatedSkillRoot, fingerprint) {
1608
+ const [state, entryExists] = await Promise.all([
1609
+ readGenerationState(generatedSkillRoot),
1610
+ pathExists(join(generatedSkillRoot, "SKILL.md"))
1611
+ ]);
1612
+ return Boolean(entryExists && state?.fingerprint === fingerprint);
1613
+ }
1614
+
1615
+ const GITHUB_LOOKUP_TIMEOUT_MS = 1500;
1616
+ function resolveStableSkillBuildDir(rootDir, buildDir) {
1617
+ const rel = relative(resolve(rootDir), resolve(buildDir)).replaceAll("\\", "/");
1618
+ if (rel === ".nuxt" || rel.startsWith(".nuxt/")) {
1619
+ return join(resolve(rootDir), ".nuxt");
1620
+ }
1621
+ return buildDir;
1622
+ }
1623
+ function normalizeRelativePath(from, to) {
1624
+ const rel = relative(from, to).replaceAll("\\", "/");
1625
+ if (!rel || rel === ".") {
1626
+ return "./";
1627
+ }
1628
+ return rel.startsWith(".") ? rel : `./${rel}`;
1629
+ }
1630
+ function getGeneratedSkillRoot(buildDir, skillName) {
1631
+ return join(buildDir, "skill-hub", skillName);
1632
+ }
1707
1633
  function mergeSkipped(entries, keyFn) {
1708
1634
  const byKey = /* @__PURE__ */ new Map();
1709
1635
  for (const entry of entries) {
@@ -1719,6 +1645,366 @@ function mergeSkipped(entries, keyFn) {
1719
1645
  }
1720
1646
  return Array.from(byKey.values()).sort((a, b) => `${a.packageName}::${a.skillName}`.localeCompare(`${b.packageName}::${b.skillName}`));
1721
1647
  }
1648
+ async function removeLegacyRootArtifacts(skillRoot) {
1649
+ await Promise.all([
1650
+ promises.rm(join(skillRoot, "references"), { recursive: true, force: true }),
1651
+ promises.rm(join(skillRoot, "manifest.json"), { force: true })
1652
+ ]);
1653
+ }
1654
+ async function collectInstalledPackages(nuxt) {
1655
+ const discoveries = [];
1656
+ const installedPackages = [];
1657
+ const seenPackageRoots = /* @__PURE__ */ new Set();
1658
+ const addInstalledPackage = (installedPackage) => {
1659
+ if (!installedPackage || seenPackageRoots.has(installedPackage.packageRoot)) {
1660
+ return;
1661
+ }
1662
+ seenPackageRoots.add(installedPackage.packageRoot);
1663
+ installedPackages.push(installedPackage);
1664
+ const discovered = discoverPackageSkillsFromInstalledPackage(installedPackage, "dist");
1665
+ if (discovered) {
1666
+ discoveries.push(discovered);
1667
+ }
1668
+ };
1669
+ const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
1670
+ const seenSpecifiers = Array.from(new Set(moduleSpecifiers));
1671
+ for (const specifier of seenSpecifiers) {
1672
+ addInstalledPackage(await discoverInstalledPackageFromSpecifier(specifier, nuxt.options.rootDir));
1673
+ }
1674
+ const layerDirectories = Array.from(new Set(
1675
+ nuxt.options._layers.map((layer) => resolve(layer.cwd || layer.config.rootDir || "")).filter((layerDir) => layerDir && layerDir !== resolve(nuxt.options.rootDir))
1676
+ ));
1677
+ for (const layerDirectory of layerDirectories) {
1678
+ addInstalledPackage(await discoverInstalledPackageFromDirectory(layerDirectory));
1679
+ }
1680
+ const localPackageSkills = await discoverLocalPackageSkills(nuxt.options.rootDir);
1681
+ if (localPackageSkills) {
1682
+ discoveries.push(localPackageSkills);
1683
+ }
1684
+ return {
1685
+ discoveries,
1686
+ installedPackages
1687
+ };
1688
+ }
1689
+ async function ensureStableSkillWrappers(input) {
1690
+ const stableBuildDir = resolveStableSkillBuildDir(input.rootDir, input.buildDir);
1691
+ const generatedSkillRoot = getGeneratedSkillRoot(stableBuildDir, input.skillName);
1692
+ const generatedEntryPath = join(generatedSkillRoot, "SKILL.md");
1693
+ for (const target of input.targets) {
1694
+ const { skillRoot } = getTargetSkillRoot(input.exportRoot, target, input.skillName);
1695
+ await ensureDir(skillRoot);
1696
+ await removeLegacyRootArtifacts(skillRoot);
1697
+ const wrapper = createStableSkillWrapper(
1698
+ input.skillName,
1699
+ normalizeRelativePath(skillRoot, generatedEntryPath),
1700
+ normalizeRelativePath(skillRoot, generatedSkillRoot),
1701
+ input.generationMode
1702
+ );
1703
+ await writeFileIfChanged(join(skillRoot, "SKILL.md"), wrapper);
1704
+ }
1705
+ }
1706
+ async function generateSkillTree(input) {
1707
+ const { nuxt, logger, options, skillName, exportRoot, generationMode } = input;
1708
+ const stableBuildDir = resolveStableSkillBuildDir(nuxt.options.rootDir, nuxt.options.buildDir);
1709
+ const generatedSkillRoot = getGeneratedSkillRoot(stableBuildDir, skillName);
1710
+ const referencesRoot = join(generatedSkillRoot, "references");
1711
+ const nuxtRoot = join(referencesRoot, "nuxt");
1712
+ const vueRoot = join(referencesRoot, "vue");
1713
+ const modulesRoot = join(referencesRoot, "modules");
1714
+ const monorepoScopePath = resolveMonorepoScopePath(nuxt.options.rootDir, exportRoot);
1715
+ const manualContributions = [];
1716
+ const contributionContext = {
1717
+ add: (contribution) => {
1718
+ manualContributions.push(contribution);
1719
+ }
1720
+ };
1721
+ const callSkillContributeHook = nuxt.callHook;
1722
+ await callSkillContributeHook("skill-hub:contribute", contributionContext);
1723
+ const { discoveries, installedPackages } = await collectInstalledPackages(nuxt);
1724
+ const discoveredContributions = await resolveContributions(discoveries);
1725
+ const workspaceDiscoverySources = collectWorkspaceDiscoverySources(nuxt.options.rootDir, discoveries);
1726
+ const resolvedManual = await Promise.all(
1727
+ manualContributions.map((contribution) => resolveManualContribution(nuxt.options.rootDir, contribution))
1728
+ );
1729
+ const fingerprint = await createGenerationFingerprint({
1730
+ packageVersion: PACKAGE_VERSION,
1731
+ rootDir: nuxt.options.rootDir,
1732
+ buildDir: nuxt.options.buildDir,
1733
+ exportRoot,
1734
+ skillName,
1735
+ options,
1736
+ installedPackages,
1737
+ localSources: await createLocalSourceFingerprints([
1738
+ ...workspaceDiscoverySources,
1739
+ ...sortAndDedupeContributions(resolvedManual)
1740
+ ])
1741
+ });
1742
+ if (generationMode === "prepare" && await isGeneratedSkillFresh(generatedSkillRoot, fingerprint)) {
1743
+ logger.info(`Generated ${skillName} skill is already up to date at ${generatedSkillRoot}`);
1744
+ return;
1745
+ }
1746
+ const distResolvedPackages = new Set(discoveredContributions.contributions.map((item) => item.packageName));
1747
+ const remoteIssues = [];
1748
+ const remoteSkipped = [];
1749
+ const remoteContributions = [];
1750
+ const remoteCacheRoot = join(stableBuildDir, "skill-hub-cache");
1751
+ await emptyDir(remoteCacheRoot);
1752
+ const packagesToResolve = installedPackages.filter((pkg) => pkg.packageName !== "nuxt-skill-hub" && !distResolvedPackages.has(pkg.packageName));
1753
+ const totalToResolve = packagesToResolve.length;
1754
+ for (let i = 0; i < packagesToResolve.length; i++) {
1755
+ const pkg = packagesToResolve[i];
1756
+ logger.start(`[${i + 1}/${totalToResolve}] Resolving skills for ${pkg.packageName}...`);
1757
+ const remote = await resolveRemoteContributionsForPackage(pkg, {
1758
+ cacheRoot: remoteCacheRoot,
1759
+ githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
1760
+ enableGithubLookup: true
1761
+ });
1762
+ remoteIssues.push(...remote.issues);
1763
+ remoteSkipped.push(...remote.skipped);
1764
+ remoteContributions.push(...remote.contributions);
1765
+ }
1766
+ const validatedRemote = await validateResolvedContributions(sortAndDedupeContributions(remoteContributions));
1767
+ const validatedManual = await validateResolvedContributions(sortAndDedupeContributions(resolvedManual));
1768
+ const contributions = sortAndDedupeContributions([
1769
+ ...discoveredContributions.contributions,
1770
+ ...validatedRemote.contributions,
1771
+ ...validatedManual.contributions
1772
+ ]);
1773
+ const validationIssues = [
1774
+ ...discoveredContributions.issues,
1775
+ ...remoteIssues,
1776
+ ...validatedRemote.issues,
1777
+ ...validatedManual.issues
1778
+ ];
1779
+ const issuesToSkipped = mergeSkipped(
1780
+ validationIssues.map((issue) => ({ packageName: issue.packageName, skillName: issue.skillName, reason: issue.reason, sourceKind: issue.sourceKind })),
1781
+ (entry) => `${entry.packageName}::${entry.skillName}`
1782
+ );
1783
+ const skipped = mergeSkipped(
1784
+ [...issuesToSkipped, ...remoteSkipped],
1785
+ (entry) => `${entry.packageName}::${entry.skillName}::${entry.sourceKind || ""}`
1786
+ );
1787
+ for (const issue of validationIssues) {
1788
+ logger.warn(`[validation] ${issue.packageName}/${issue.skillName}: ${issue.reason}`);
1789
+ }
1790
+ await emptyDir(generatedSkillRoot);
1791
+ await ensureDir(nuxtRoot);
1792
+ await ensureDir(vueRoot);
1793
+ await ensureDir(modulesRoot);
1794
+ const nuxtTemplateFiles = await buildNuxtTemplateFiles(nuxtRoot);
1795
+ for (const file of nuxtTemplateFiles) {
1796
+ await writeFileIfChanged(file.path, file.contents);
1797
+ }
1798
+ const nuxtIndexTemplatePath = join(nuxtRoot, "index.template.md");
1799
+ const nuxtIndexTemplate = await pathExists(nuxtIndexTemplatePath) ? await promises.readFile(nuxtIndexTemplatePath, "utf8") : "";
1800
+ const nuxtIndexContent = await renderAutomdTemplate(nuxtIndexTemplate, nuxtRoot);
1801
+ await writeFileIfChanged(join(nuxtRoot, "index.md"), nuxtIndexContent);
1802
+ logger.start("Fetching Vue best-practices content...");
1803
+ const vueTemplateFiles = await buildVueTemplateFiles(vueRoot, remoteCacheRoot);
1804
+ for (const file of vueTemplateFiles) {
1805
+ await writeFileIfChanged(file.path, file.contents);
1806
+ }
1807
+ const generatedEntries = [];
1808
+ for (const contribution of contributions) {
1809
+ const includeScripts = contribution.forceIncludeScripts;
1810
+ const destination = createModuleDestination(modulesRoot, contribution);
1811
+ await copySkillTree(contribution.sourceDir, destination, includeScripts);
1812
+ const entryPath = relative(generatedSkillRoot, join(destination, "SKILL.md")).replaceAll("\\", "/");
1813
+ generatedEntries.push({
1814
+ packageName: contribution.packageName,
1815
+ version: contribution.version,
1816
+ skillName: contribution.skillName,
1817
+ entryPath,
1818
+ sourceDir: contribution.sourceDir,
1819
+ destination: relative(generatedSkillRoot, destination).replaceAll("\\", "/"),
1820
+ scriptsIncluded: includeScripts,
1821
+ description: contribution.description,
1822
+ sourceKind: contribution.sourceKind,
1823
+ sourceLabel: getSourceLabel(contribution.sourceKind),
1824
+ sourceRepo: contribution.sourceRepo,
1825
+ sourceRef: contribution.sourceRef,
1826
+ sourcePath: contribution.sourcePath,
1827
+ repoUrl: contribution.repoUrl,
1828
+ docsUrl: contribution.docsUrl,
1829
+ official: contribution.official,
1830
+ trustLevel: getTrustLevel(contribution.official),
1831
+ resolver: contribution.resolver
1832
+ });
1833
+ }
1834
+ const nuxtMetadata = await loadNuxtMetadata();
1835
+ await writeFileIfChanged(join(generatedSkillRoot, "SKILL.md"), createSkillEntrypoint(
1836
+ skillName,
1837
+ nuxtMetadata,
1838
+ monorepoScopePath,
1839
+ Boolean(options.moduleAuthoring),
1840
+ generatedEntries,
1841
+ skipped
1842
+ ));
1843
+ await writeGenerationState(generatedSkillRoot, fingerprint);
1844
+ logger.success(`Generated ${skillName} skill at ${generatedSkillRoot}`);
1845
+ }
1846
+
1847
+ function isCancelled(value) {
1848
+ return value === null || typeof value === "symbol";
1849
+ }
1850
+ async function runInstallWizard(nuxt) {
1851
+ const logger = useLogger("nuxt-skill-hub");
1852
+ if (process.env.CI || !process.stdout.isTTY) {
1853
+ logger.info("Non-interactive environment detected, skipping install wizard.");
1854
+ return;
1855
+ }
1856
+ const consola = createConsola();
1857
+ const rootDir = nuxt.options.rootDir;
1858
+ const exportRoot = await resolveExportRoot(rootDir);
1859
+ const skillName = await deriveSkillName(rootDir);
1860
+ const pendingWrites = [];
1861
+ consola.box(
1862
+ `nuxt-skill-hub \u2014 First-time setup
1863
+ Let's configure AI agent skills for your Nuxt project.`
1864
+ );
1865
+ const currentTarget = detectCurrentTarget();
1866
+ let selectedTargets;
1867
+ if (currentTarget) {
1868
+ consola.info(`Running inside ${currentTarget}, auto-selecting it.`);
1869
+ selectedTargets = [currentTarget];
1870
+ } else {
1871
+ const detectedTargets = detectInstalledTargets();
1872
+ const allTargets = getSupportedTargets();
1873
+ if (!detectedTargets.length && !allTargets.length) {
1874
+ consola.warn("No AI agents detected. Skills will be generated when an agent is detected.");
1875
+ consola.info("Supported agents are sourced from unagent.");
1876
+ return;
1877
+ }
1878
+ consola.info(`Detected agents: ${detectedTargets.length ? detectedTargets.join(", ") : "none"}`);
1879
+ selectedTargets = detectedTargets;
1880
+ if (detectedTargets.length > 1) {
1881
+ const keepAll = await consola.prompt("Generate skills for all detected agents?", {
1882
+ type: "confirm",
1883
+ initial: true,
1884
+ cancel: "null"
1885
+ });
1886
+ if (isCancelled(keepAll)) {
1887
+ consola.info("Setup cancelled.");
1888
+ return;
1889
+ }
1890
+ if (!keepAll) {
1891
+ const chosen = await consola.prompt("Select agents to generate skills for:", {
1892
+ type: "multiselect",
1893
+ options: detectedTargets.map((t) => ({ label: t, value: t })),
1894
+ initial: detectedTargets,
1895
+ required: true,
1896
+ cancel: "null"
1897
+ });
1898
+ if (isCancelled(chosen)) {
1899
+ consola.info("Setup cancelled.");
1900
+ return;
1901
+ }
1902
+ selectedTargets = chosen.map((c) => c.value);
1903
+ }
1904
+ }
1905
+ }
1906
+ const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
1907
+ const uniqueSpecifiers = Array.from(new Set(moduleSpecifiers));
1908
+ const installedModulePackages = [];
1909
+ for (const specifier of uniqueSpecifiers) {
1910
+ const pkg = await discoverInstalledPackageFromSpecifier(specifier, rootDir);
1911
+ if (!pkg || pkg.packageName === "nuxt-skill-hub")
1912
+ continue;
1913
+ installedModulePackages.push(pkg.packageName);
1914
+ }
1915
+ if (installedModulePackages.length) {
1916
+ consola.info(`Detected ${installedModulePackages.length} installed module package(s):`);
1917
+ for (const packageName of installedModulePackages) {
1918
+ consola.log(` ${packageName}`);
1919
+ }
1920
+ consola.info("Module skills and metadata routers are resolved automatically at build time.");
1921
+ } else {
1922
+ consola.info("No installed module entries were discovered.");
1923
+ }
1924
+ for (const conflict of detectConflictingSkills(selectedTargets, skillName)) {
1925
+ consola.warn(formatConflictWarning(conflict));
1926
+ }
1927
+ const hintBlock = `${MANAGED_HINT_START}
1928
+ Use the \`${skillName}\` skill as the Nuxt router/entrypoint for tasks in this repository.
1929
+ ${MANAGED_HINT_END}`;
1930
+ const hintFileChoice = await consola.prompt("Add a skill hint to help agents discover the skill?", {
1931
+ type: "select",
1932
+ options: [
1933
+ { label: "CLAUDE.md", value: "claude", hint: "Claude Code project instructions" },
1934
+ { label: "AGENTS.md", value: "agents", hint: "Universal agent instructions" },
1935
+ { label: "Both", value: "both" },
1936
+ { label: "Skip", value: "skip" }
1937
+ ],
1938
+ cancel: "null"
1939
+ });
1940
+ if (isCancelled(hintFileChoice)) {
1941
+ consola.info("Setup cancelled.");
1942
+ return;
1943
+ }
1944
+ const addToFile = async (filename) => {
1945
+ const filePath = join(exportRoot, filename);
1946
+ const exists = await pathExists(filePath);
1947
+ const current = exists ? readFileSync(filePath, "utf8") : "";
1948
+ const regex = new RegExp(`${MANAGED_HINT_START}[\\s\\S]*?${MANAGED_HINT_END}`);
1949
+ let newContent;
1950
+ if (regex.test(current)) {
1951
+ newContent = current.replace(regex, hintBlock);
1952
+ } else if (current) {
1953
+ newContent = `${current.trimEnd()}
1954
+
1955
+ ${hintBlock}
1956
+ `;
1957
+ } else {
1958
+ newContent = `${hintBlock}
1959
+ `;
1960
+ }
1961
+ pendingWrites.push({
1962
+ file: filename,
1963
+ absPath: filePath,
1964
+ preview: `+ Use the \`${skillName}\` skill as the Nuxt router/entrypoint for tasks in this repository.`,
1965
+ content: newContent,
1966
+ action: exists ? "modify" : "create"
1967
+ });
1968
+ };
1969
+ if (hintFileChoice === "claude" || hintFileChoice === "both") {
1970
+ await addToFile("CLAUDE.md");
1971
+ }
1972
+ if (hintFileChoice === "agents" || hintFileChoice === "both") {
1973
+ await addToFile("AGENTS.md");
1974
+ }
1975
+ if (!pendingWrites.length) {
1976
+ consola.success("No file changes needed. Setup complete!");
1977
+ consola.info("Run `nuxt prepare` to generate the build-dir skill output.");
1978
+ return;
1979
+ }
1980
+ consola.log("");
1981
+ consola.info("Planned changes:");
1982
+ for (const write of pendingWrites) {
1983
+ consola.box({ title: write.file, message: colorize("green", write.preview), style: { borderColor: "green" } });
1984
+ }
1985
+ const confirm = await consola.prompt("Apply these changes?", {
1986
+ type: "confirm",
1987
+ initial: true,
1988
+ cancel: "null"
1989
+ });
1990
+ if (isCancelled(confirm) || !confirm) {
1991
+ consola.info("Setup cancelled. No files were modified.");
1992
+ return;
1993
+ }
1994
+ for (const write of pendingWrites) {
1995
+ writeFileSync(write.absPath, write.content, "utf8");
1996
+ const verb = write.action === "create" ? "Created" : "Updated";
1997
+ consola.success(`${verb} ${write.file}`);
1998
+ }
1999
+ consola.log("");
2000
+ consola.box(
2001
+ `Setup complete!
2002
+
2003
+ Run \`nuxt prepare\` to generate the build-dir skill output.
2004
+ Configure \`skillHub.skillName\`, \`skillHub.targets\`, or set \`skillHub.generationMode: 'manual'\` in nuxt.config.ts if needed.`
2005
+ );
2006
+ }
2007
+
1722
2008
  const module$1 = defineNuxtModule({
1723
2009
  meta: {
1724
2010
  name: "nuxt-skill-hub",
@@ -1734,7 +2020,8 @@ const module$1 = defineNuxtModule({
1734
2020
  defaults: {
1735
2021
  skillName: "",
1736
2022
  targets: [],
1737
- moduleAuthoring: false
2023
+ moduleAuthoring: false,
2024
+ generationMode: DEFAULT_SKILL_HUB_GENERATION_MODE
1738
2025
  },
1739
2026
  async setup(options, nuxt) {
1740
2027
  const logger = useLogger("nuxt-skill-hub");
@@ -1742,7 +2029,8 @@ const module$1 = defineNuxtModule({
1742
2029
  logger.info("Skipping skill generation in CI");
1743
2030
  return;
1744
2031
  }
1745
- if (nuxt.options._prepare) {
2032
+ if (process.argv.slice(2).includes("typecheck")) {
2033
+ logger.info("Skipping skill generation during typecheck");
1746
2034
  return;
1747
2035
  }
1748
2036
  const configuredSkillName = options.skillName?.trim();
@@ -1755,173 +2043,52 @@ const module$1 = defineNuxtModule({
1755
2043
  }
1756
2044
  resolvedSkillName = await deriveSkillName(nuxt.options.rootDir);
1757
2045
  }
1758
- nuxt.hook("modules:done", async () => {
1759
- const exportRoot = await resolveExportRoot(nuxt.options.rootDir);
1760
- const monorepoScopePath = resolveMonorepoScopePath(nuxt.options.rootDir, exportRoot);
1761
- const manualContributions = [];
1762
- const contributionContext = {
1763
- add: (contribution) => {
1764
- manualContributions.push(contribution);
1765
- }
1766
- };
1767
- const callSkillContributeHook = nuxt.callHook;
1768
- await callSkillContributeHook("skill-hub:contribute", contributionContext);
1769
- const discoveries = [];
1770
- const installedPackages = [];
1771
- const seenPackageRoots = /* @__PURE__ */ new Set();
1772
- const addInstalledPackage = (installedPackage) => {
1773
- if (!installedPackage || seenPackageRoots.has(installedPackage.packageRoot)) {
1774
- return;
1775
- }
1776
- seenPackageRoots.add(installedPackage.packageRoot);
1777
- installedPackages.push(installedPackage);
1778
- const discovered = discoverPackageSkillsFromInstalledPackage(installedPackage, "dist");
1779
- if (discovered) {
1780
- discoveries.push(discovered);
1781
- }
1782
- };
1783
- const moduleSpecifiers = (nuxt.options.modules || []).map((entry) => extractModuleSpecifier(entry)).filter((entry) => Boolean(entry));
1784
- const seenSpecifiers = Array.from(new Set(moduleSpecifiers));
1785
- for (const specifier of seenSpecifiers) {
1786
- addInstalledPackage(await discoverInstalledPackageFromSpecifier(specifier, nuxt.options.rootDir));
1787
- }
1788
- const layerDirectories = Array.from(new Set(
1789
- nuxt.options._layers.map((layer) => resolve(layer.cwd || layer.config.rootDir || "")).filter((layerDir) => layerDir && layerDir !== resolve(nuxt.options.rootDir))
1790
- ));
1791
- for (const layerDirectory of layerDirectories) {
1792
- addInstalledPackage(await discoverInstalledPackageFromDirectory(layerDirectory));
1793
- }
1794
- const localPackageSkills = await discoverLocalPackageSkills(nuxt.options.rootDir);
1795
- if (localPackageSkills) {
1796
- discoveries.push(localPackageSkills);
1797
- }
1798
- const discoveredContributions = await resolveContributions(discoveries);
1799
- const distResolvedPackages = new Set(discoveredContributions.contributions.map((item) => item.packageName));
1800
- const remoteIssues = [];
1801
- const remoteSkipped = [];
1802
- const remoteContributions = [];
1803
- const remoteCacheRoot = join(nuxt.options.rootDir, ".nuxt", "skill-hub-cache");
1804
- await emptyDir(remoteCacheRoot);
1805
- const packagesToResolve = installedPackages.filter((pkg) => pkg.packageName !== "nuxt-skill-hub" && !distResolvedPackages.has(pkg.packageName));
1806
- const totalToResolve = packagesToResolve.length;
1807
- for (let i = 0; i < packagesToResolve.length; i++) {
1808
- const pkg = packagesToResolve[i];
1809
- logger.start(`[${i + 1}/${totalToResolve}] Resolving skills for ${pkg.packageName}...`);
1810
- const remote = await resolveRemoteContributionsForPackage(pkg, {
1811
- cacheRoot: remoteCacheRoot,
1812
- githubLookupTimeoutMs: GITHUB_LOOKUP_TIMEOUT_MS,
1813
- enableGithubLookup: true
1814
- });
1815
- remoteIssues.push(...remote.issues);
1816
- remoteSkipped.push(...remote.skipped);
1817
- remoteContributions.push(...remote.contributions);
1818
- }
1819
- const validatedRemote = await validateResolvedContributions(sortAndDedupeContributions(remoteContributions));
1820
- const resolvedManual = await Promise.all(
1821
- manualContributions.map((contribution) => resolveManualContribution(nuxt.options.rootDir, contribution))
1822
- );
1823
- const validatedManual = await validateResolvedContributions(sortAndDedupeContributions(resolvedManual));
1824
- const contributions = sortAndDedupeContributions([
1825
- ...discoveredContributions.contributions,
1826
- ...validatedRemote.contributions,
1827
- ...validatedManual.contributions
1828
- ]);
1829
- const validationIssues = [
1830
- ...discoveredContributions.issues,
1831
- ...remoteIssues,
1832
- ...validatedRemote.issues,
1833
- ...validatedManual.issues
1834
- ];
1835
- const issuesToSkipped = mergeSkipped(
1836
- validationIssues.map((i) => ({ packageName: i.packageName, skillName: i.skillName, reason: i.reason, sourceKind: i.sourceKind })),
1837
- (e) => `${e.packageName}::${e.skillName}`
1838
- );
1839
- const skipped = mergeSkipped(
1840
- [...issuesToSkipped, ...remoteSkipped],
1841
- (e) => `${e.packageName}::${e.skillName}::${e.sourceKind || ""}`
1842
- );
1843
- for (const issue of validationIssues) {
1844
- logger.warn(`[validation] ${issue.packageName}/${issue.skillName}: ${issue.reason}`);
1845
- }
1846
- const { targets, invalidTargets } = resolveTargets(options.targets || []);
1847
- for (const invalidTarget of invalidTargets) {
1848
- if (invalidTarget.reason === "unknown-target") {
1849
- logger.warn(`Target "${invalidTarget.target}" is unknown in unagent and was skipped.`);
1850
- continue;
1851
- }
1852
- logger.warn(`Target "${invalidTarget.target}" has no skillsDir in unagent and was skipped.`);
1853
- }
1854
- if (!targets.length) {
1855
- logger.warn("No detected targets. Set skillHub.targets to force generation for specific agents.");
1856
- return;
1857
- }
1858
- for (const conflict of detectConflictingSkills(targets, resolvedSkillName)) {
1859
- logger.warn(formatConflictWarning(conflict));
1860
- }
1861
- const nuxtMetadata = await loadNuxtMetadata();
1862
- for (let targetIdx = 0; targetIdx < targets.length; targetIdx++) {
1863
- const target = targets[targetIdx];
1864
- const { skillRoot } = getTargetSkillRoot(exportRoot, target, resolvedSkillName);
1865
- const referencesRoot = join(skillRoot, "references");
1866
- const nuxtRoot = join(referencesRoot, "nuxt");
1867
- const vueRoot = join(referencesRoot, "vue");
1868
- const modulesRoot = join(referencesRoot, "modules");
1869
- await emptyDir(skillRoot);
1870
- await ensureDir(nuxtRoot);
1871
- await ensureDir(vueRoot);
1872
- await ensureDir(modulesRoot);
1873
- const nuxtTemplateFiles = await buildNuxtTemplateFiles(nuxtRoot);
1874
- for (const file of nuxtTemplateFiles) {
1875
- await writeFileIfChanged(file.path, file.contents);
1876
- }
1877
- const nuxtIndexTemplatePath = join(nuxtRoot, "index.template.md");
1878
- const nuxtIndexTemplate = await pathExists(nuxtIndexTemplatePath) ? await promises.readFile(nuxtIndexTemplatePath, "utf8") : "";
1879
- const nuxtIndexContent = await renderAutomdTemplate(nuxtIndexTemplate, nuxtRoot);
1880
- await writeFileIfChanged(join(nuxtRoot, "index.md"), nuxtIndexContent);
1881
- if (targetIdx === 0)
1882
- logger.start("Fetching Vue best-practices content...");
1883
- const vueTemplateFiles = await buildVueTemplateFiles(vueRoot, remoteCacheRoot);
1884
- for (const file of vueTemplateFiles) {
1885
- await writeFileIfChanged(file.path, file.contents);
1886
- }
1887
- const generatedEntries = [];
1888
- for (const contribution of contributions) {
1889
- const includeScripts = contribution.forceIncludeScripts;
1890
- const destination = createModuleDestination(modulesRoot, contribution);
1891
- await copySkillTree(contribution.sourceDir, destination, includeScripts);
1892
- const entryPath = relative(skillRoot, join(destination, "SKILL.md"));
1893
- generatedEntries.push({
1894
- packageName: contribution.packageName,
1895
- version: contribution.version,
1896
- skillName: contribution.skillName,
1897
- entryPath,
1898
- sourceDir: contribution.sourceDir,
1899
- destination: relative(skillRoot, destination),
1900
- scriptsIncluded: includeScripts,
1901
- description: contribution.description,
1902
- sourceKind: contribution.sourceKind,
1903
- sourceLabel: getSourceLabel(contribution.sourceKind),
1904
- sourceRepo: contribution.sourceRepo,
1905
- sourceRef: contribution.sourceRef,
1906
- sourcePath: contribution.sourcePath,
1907
- repoUrl: contribution.repoUrl,
1908
- docsUrl: contribution.docsUrl,
1909
- official: contribution.official,
1910
- trustLevel: getTrustLevel(contribution.official),
1911
- resolver: contribution.resolver
1912
- });
1913
- }
1914
- await writeFileIfChanged(join(skillRoot, "SKILL.md"), createSkillEntrypoint(
1915
- resolvedSkillName,
1916
- nuxtMetadata,
1917
- monorepoScopePath,
1918
- Boolean(options.moduleAuthoring),
1919
- generatedEntries,
1920
- skipped
1921
- ));
1922
- logger.success(`Generated ${resolvedSkillName} skill at ${skillRoot}`);
2046
+ const exportRoot = await resolveExportRoot(nuxt.options.rootDir);
2047
+ const rawGenerationMode = options.generationMode;
2048
+ const generationMode = normalizeGenerationMode(rawGenerationMode);
2049
+ const { targets, invalidTargets } = resolveTargets(options.targets || []);
2050
+ if (rawGenerationMode && rawGenerationMode !== generationMode) {
2051
+ logger.warn(`Invalid skillHub.generationMode "${rawGenerationMode}". Falling back to "prepare".`);
2052
+ }
2053
+ for (const invalidTarget of invalidTargets) {
2054
+ if (invalidTarget.reason === "unknown-target") {
2055
+ logger.warn(`Target "${invalidTarget.target}" is unknown in unagent and was skipped.`);
2056
+ continue;
1923
2057
  }
2058
+ logger.warn(`Target "${invalidTarget.target}" has no skillsDir in unagent and was skipped.`);
2059
+ }
2060
+ if (!targets.length) {
2061
+ logger.warn("No detected targets. Set skillHub.targets to force generation for specific agents.");
2062
+ return;
2063
+ }
2064
+ await ensureStableSkillWrappers({
2065
+ rootDir: nuxt.options.rootDir,
2066
+ exportRoot,
2067
+ buildDir: nuxt.options.buildDir,
2068
+ skillName: resolvedSkillName,
2069
+ targets,
2070
+ generationMode
1924
2071
  });
2072
+ for (const conflict of detectConflictingSkills(targets, resolvedSkillName)) {
2073
+ logger.warn(formatConflictWarning(conflict));
2074
+ }
2075
+ if (generationMode === "manual") {
2076
+ return;
2077
+ }
2078
+ const runGeneration = async () => {
2079
+ await generateSkillTree({
2080
+ nuxt,
2081
+ logger,
2082
+ options,
2083
+ skillName: resolvedSkillName,
2084
+ exportRoot,
2085
+ generationMode
2086
+ });
2087
+ };
2088
+ if (generationMode !== "prepare" || !nuxt.options._prepare) {
2089
+ return;
2090
+ }
2091
+ nuxt.hook("prepare:types", runGeneration);
1925
2092
  }
1926
2093
  });
1927
2094