diff --git a/packages/nuxi/src/commands/module/_skills.ts b/packages/nuxi/src/commands/module/_skills.ts new file mode 100644 index 00000000..222a64bf --- /dev/null +++ b/packages/nuxi/src/commands/module/_skills.ts @@ -0,0 +1,85 @@ +import { spinner } from '@clack/prompts' +import { join } from 'pathe' +import { x } from 'tinyexec' + +// Types from @nuxt/schema (PR 1) - defined locally until schema is updated +interface ModuleAgentSkillsConfig { + url: string + skills?: string[] +} + +interface ModuleAgentsConfig { + skills?: ModuleAgentSkillsConfig +} + +interface ModuleMeta { + name?: string + agents?: ModuleAgentsConfig +} + +export interface ModuleSkillInfo { + url: string + skills?: string[] + moduleName: string +} + +export async function detectModuleSkills(moduleNames: string[], cwd: string): Promise { + const result: ModuleSkillInfo[] = [] + + for (const pkgName of moduleNames) { + const meta = await getModuleMeta(pkgName, cwd) + if (meta?.agents?.skills?.url) { + result.push({ + url: meta.agents.skills.url, + skills: meta.agents.skills.skills, + moduleName: pkgName, + }) + } + } + return result +} + +async function getModuleMeta(pkgName: string, cwd: string): Promise { + try { + const modulePath = join(cwd, 'node_modules', pkgName) + const mod = await import(modulePath) + return await mod?.default?.getMeta?.() + } + catch { + return null + } +} + +export async function installSkills(infos: ModuleSkillInfo[], cwd: string): Promise { + for (const info of infos) { + const skills = info.skills ?? [] + const label = skills.length > 0 ? `Installing ${skills.join(', ')}...` : `Installing skills from ${info.url}...` + + const s = spinner() + s.start(label) + + try { + const args = ['skills', 'add', info.url, '-y'] + if (skills.length > 0) { + args.push('--skill', ...skills) + } + + await x('npx', args, { + nodeOptions: { cwd, stdio: 'pipe' }, + }) + + s.stop('Installed to detected agents') + } + catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error) + s.stop('Failed to install skills') + console.warn(`Skill installation failed: ${msg}`) + } + } +} + +export function getSkillNames(infos: ModuleSkillInfo[]): string { + return infos + .flatMap(i => i.skills?.length ? i.skills : ['all']) + .join(', ') +} diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index b4286535..7594f3e2 100644 --- a/packages/nuxi/src/commands/module/add.ts +++ b/packages/nuxi/src/commands/module/add.ts @@ -8,7 +8,7 @@ import { homedir } from 'node:os' import { join } from 'node:path' import process from 'node:process' -import { cancel, confirm, isCancel, select } from '@clack/prompts' +import { cancel, confirm, isCancel, select, spinner } from '@clack/prompts' import { updateConfig } from 'c12/update' import { defineCommand } from 'citty' import { colors } from 'consola/utils' @@ -25,6 +25,7 @@ import { relativeToProcess } from '../../utils/paths' import { getNuxtVersion } from '../../utils/versions' import { cwdArgs, logLevelArgs } from '../_shared' import prepareCommand from '../prepare' +import { detectModuleSkills, getSkillNames, installSkills } from './_skills' import { checkNuxtCompatibility, fetchModules, getRegistryFromContent } from './_utils' interface RegistryMeta { @@ -101,6 +102,27 @@ export default defineCommand({ await addModules(resolvedModules, { ...ctx.args, cwd }, projectPkg) + // Check for agent skills + if (!ctx.args.skipInstall) { + const moduleNames = resolvedModules.map(m => m.pkgName) + const checkSpinner = spinner() + checkSpinner.start('Checking for agent skills...') + const skillInfos = await detectModuleSkills(moduleNames, cwd) + checkSpinner.stop(skillInfos.length > 0 ? `Found ${skillInfos.length} skill(s)` : 'No skills found') + + if (skillInfos.length > 0) { + const skillNames = getSkillNames(skillInfos) + const shouldInstall = await confirm({ + message: `Install agent skill(s): ${skillNames}?`, + initialValue: true, + }) + + if (!isCancel(shouldInstall) && shouldInstall) { + await installSkills(skillInfos, cwd) + } + } + } + // Run prepare command if install is not skipped if (!ctx.args.skipInstall) { const args = Object.entries(ctx.args).filter(([k]) => k in cwdArgs || k in logLevelArgs).map(([k, v]) => `--${k}=${v}`)