Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/nuxi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"tsdown": "^0.19.0",
"typescript": "^5.9.3",
"ufo": "^1.6.3",
"unagent": "^0.0.5",
"unplugin-purge-polyfills": "^0.1.0",
"vitest": "^3.2.4",
"youch": "^4.1.0-beta.13"
Expand Down
130 changes: 130 additions & 0 deletions packages/nuxi/src/commands/module/_skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { BatchInstallCallbacks, InstallSkillResult, SkillSource } from 'unagent'
import { createRequire } from 'node:module'
import { spinner } from '@clack/prompts'
import { detectInstalledAgents, formatDetectedAgentIds, installSkill } from 'unagent'

import { logger } from '../../utils/logger'

// TODO: Import from @nuxt/schema when nuxt/nuxt#34187 is merged
interface ModuleAgentSkillsConfig { url: string, skills?: string[] }
interface ModuleAgentsConfig { skills?: ModuleAgentSkillsConfig }
interface ModuleMeta { name?: string, agents?: ModuleAgentsConfig }

export interface ModuleSkillSource extends SkillSource {
moduleName: string
isLocal: boolean
}

/**
* Detect skills from module meta (meta.agents.skills.url)
*/
export async function detectModuleSkills(moduleNames: string[], cwd: string): Promise<ModuleSkillSource[]> {
const result: ModuleSkillSource[] = []

for (const pkgName of moduleNames) {
const meta = await getModuleMeta(pkgName, cwd)
if (meta?.agents?.skills?.url) {
result.push({
source: meta.agents.skills.url,
skills: meta.agents.skills.skills,
label: pkgName,
moduleName: pkgName,
isLocal: false,
mode: 'copy',
})
}
}
return result
}

async function getModuleMeta(pkgName: string, cwd: string): Promise<ModuleMeta | null> {
try {
const require = createRequire(`${cwd}/`)
const modulePath = require.resolve(pkgName)
const mod = await import(modulePath)
const meta: unknown = await mod?.default?.getMeta?.()
if (meta && typeof meta === 'object')
return meta as ModuleMeta
return null
}
catch {
return null
}
}

export interface InstallModuleSkillOptions {
agents?: string[]
}

export async function installModuleSkills(sources: ModuleSkillSource[], options: InstallModuleSkillOptions = {}): Promise<void> {
const installedAgents = detectInstalledAgents()
if (installedAgents.length === 0) {
logger.warn('No AI coding agents detected')
return
}

const targetAgents = options.agents?.length
? installedAgents.filter(agent => options.agents!.includes(agent.id))
: installedAgents

if (targetAgents.length === 0) {
logger.warn('No matching AI coding agents detected')
return
}

const agentNames = formatDetectedAgentIds(targetAgents)

const callbacks: BatchInstallCallbacks = {
onStart: (source: SkillSource) => {
const info = source as ModuleSkillSource
const skills = info.skills ?? []
const label = skills.length > 0
? `Installing ${skills.join(', ')} from ${info.moduleName}...`
: `Installing skills from ${info.moduleName}...`
const s = spinner()
s.start(label)
;(source as ModuleSkillSource & { _spinner: typeof s })._spinner = s
},
onSuccess: (source: SkillSource, result: InstallSkillResult) => {
const info = source as ModuleSkillSource & { _spinner: ReturnType<typeof spinner> }
if (result.installed.length > 0) {
const skillNames = [...new Set(result.installed.map((i: { skill: string }) => i.skill))].join(', ')
const mode = info.isLocal ? 'linked' : 'installed'
info._spinner?.stop(`${mode} ${skillNames} → ${agentNames}`)
}
else {
info._spinner?.stop('No skills to install')
}
},
onError: (source: SkillSource, error: string) => {
const info = source as ModuleSkillSource & { _spinner: ReturnType<typeof spinner> }
const isAlreadyInstalled = error.includes('Cannot overwrite directory') || error.includes('EEXIST')
if (isAlreadyInstalled) {
info._spinner?.stop('Already installed')
return
}
info._spinner?.stop('Failed to install skills')
logger.warn(`Skill installation failed for ${info.moduleName}: ${error}`)
},
}

for (const source of sources) {
callbacks.onStart?.(source)
try {
const result = await installSkill({
source: source.source,
skills: source.skills,
mode: source.mode ?? 'copy',
agents: options.agents?.length ? options.agents : undefined,
})
if (result.installed.length > 0)
callbacks.onSuccess?.(source, result)
if (result.errors.length > 0)
callbacks.onError?.(source, result.errors.map(e => e.error).join(', '))
}
catch (error) {
const message = error instanceof Error ? error.message : String(error)
callbacks.onError?.(source, message)
}
}
}
Loading
Loading