Skip to content
Open
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",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

unagent npm package latest version stability

💡 Result:

I couldn’t verify a current npm package named unagent on the public npm registry from web search results (no npmjs.com/package/unagent listing showed up), so I can’t report a “latest version” or assess release cadence/stability for an npm package with that exact name.

What I did find is “Unagent” as a Safari (iOS/macOS) extension project on GitHub, which is not an npm package. [1]

If you meant a different package (e.g., scoped like @scope/unagent, hyphenated like un-agent, or similarly named), tell me the exact npm name and I’ll look up its latest published version + stability signals (publish date history, weekly downloads, open issues, etc.).

Sources: [1]


🏁 Script executed:

cat -n packages/nuxi/package.json | grep -A 5 -B 5 "unagent"

Repository: nuxt/cli

Length of output: 381


🏁 Script executed:

rg "unagent" --type json --type js --type ts -i

Repository: nuxt/cli

Length of output: 1248


🏁 Script executed:

find . -name "package.json" -exec grep -l '"name".*:.*"unagent"' {} \;

Repository: nuxt/cli

Length of output: 34


🏁 Script executed:

fd -t d unagent

Repository: nuxt/cli

Length of output: 34


🏁 Script executed:

cat .npmrc 2>/dev/null || echo "No .npmrc found"

Repository: nuxt/cli

Length of output: 67


🏁 Script executed:

rg "\"name\".*:.*unagent|workspace.*unagent" --type json

Repository: nuxt/cli

Length of output: 34


🏁 Script executed:

cat -i package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null | grep -A 3 "unagent" | head -50

Repository: nuxt/cli

Length of output: 34


🏁 Script executed:

git log --oneline -p packages/nuxi/package.json | grep -A 3 -B 3 "unagent" | head -20

Repository: nuxt/cli

Length of output: 253


Remove or clarify the non-existent unagent dependency.

unagent does not exist as a public npm package and is not defined as a local workspace package. However, the codebase actively imports from it in packages/nuxi/src/commands/module/{add,skills,_skills}.ts. This will cause module resolution failures at runtime. Either this is a missing private registry configuration, a typo for an actual package name, or the dependency and its usages need to be removed entirely.

🤖 Prompt for AI Agents
In `@packages/nuxi/package.json` at line 77, The package.json lists a non-existent
dependency "unagent" which causes runtime module resolution failures because
code in packages/nuxi/src/commands/module/add.ts, skills.ts and _skills.ts
import from it; either remove "unagent" from package.json and delete or refactor
its imports in those three files, or replace the entry with the correct package
name/registry and update package install configuration; specifically locate
imports of "unagent" in the three modules (import ... from 'unagent'), then
either (A) change those imports to the correct, existing package name used
elsewhere in the repo or a publicly available alternative, or (B) remove the
imports and their usage and implement equivalent logic, or (C) restore/add the
private registry/config that provides "unagent" and document it in package.json
so installs succeed.

"unplugin-purge-polyfills": "^0.1.0",
"vitest": "^3.2.4",
"youch": "^4.1.0-beta.13"
Expand Down
132 changes: 132 additions & 0 deletions packages/nuxi/src/commands/module/_skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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 requested = options.agents?.length ? new Set(options.agents) : null
const targetAgents = requested
? installedAgents.filter(agent => requested.has(agent.id))
: installedAgents

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

const agentNames = formatDetectedAgentIds(targetAgents)
const effectiveAgentIds = requested ? targetAgents.map(agent => agent.id) : undefined

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: effectiveAgentIds,
})
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)
}
}
Comment on lines +113 to +131
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: Spinner never stopped when installSkill returns empty results.

If result.installed and result.errors are both empty arrays, neither onSuccess nor onError is called, leaving the spinner running indefinitely. The else branch in onSuccess (line 97–99) that would stop the spinner with "No skills to install" is dead code since onSuccess is only called when result.installed.length > 0.

🐛 Proposed fix
     for (const source of sources) {
       callbacks.onStart?.(source)
       try {
         const result = await installSkill({
           source: source.source,
           skills: source.skills,
           mode: source.mode ?? 'copy',
           agents: effectiveAgentIds,
         })
-      if (result.installed.length > 0)
-        callbacks.onSuccess?.(source, result)
-      if (result.errors.length > 0)
+      if (result.installed.length > 0) {
+        callbacks.onSuccess?.(source, result)
+      }
+      if (result.errors.length > 0) {
         callbacks.onError?.(source, result.errors.map(e => e.error).join(', '))
+      }
+      if (result.installed.length === 0 && result.errors.length === 0) {
+        const info = source as ModuleSkillSource & { _spinner: ReturnType<typeof spinner> }
+        info._spinner?.stop('No skills to install')
+      }
       }
       catch (error) {
         const message = error instanceof Error ? error.message : String(error)
         callbacks.onError?.(source, message)
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const source of sources) {
callbacks.onStart?.(source)
try {
const result = await installSkill({
source: source.source,
skills: source.skills,
mode: source.mode ?? 'copy',
agents: effectiveAgentIds,
})
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)
}
}
for (const source of sources) {
callbacks.onStart?.(source)
try {
const result = await installSkill({
source: source.source,
skills: source.skills,
mode: source.mode ?? 'copy',
agents: effectiveAgentIds,
})
if (result.installed.length > 0) {
callbacks.onSuccess?.(source, result)
}
if (result.errors.length > 0) {
callbacks.onError?.(source, result.errors.map(e => e.error).join(', '))
}
if (result.installed.length === 0 && result.errors.length === 0) {
const info = source as ModuleSkillSource & { _spinner: ReturnType<typeof spinner> }
info._spinner?.stop('No skills to install')
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error)
callbacks.onError?.(source, message)
}
}
🤖 Prompt for AI Agents
In `@packages/nuxi/src/commands/module/_skills.ts` around lines 113 - 131, The
loop calling installSkill checks result.installed and result.errors but
currently only invokes callbacks.onSuccess when installed.length>0, so the
spinner isn't stopped if both arrays are empty; change the post-install logic in
the for loop (the block handling the result from installSkill) to treat the
"both empty" case as a success/empty case by invoking
callbacks.onSuccess?.(source, result) when result.installed.length === 0 &&
result.errors.length === 0 (or simply call onSuccess when installed.length > 0
OR both arrays are empty), keep invoking callbacks.onError?.(source, ...) when
result.errors.length > 0, and preserve the existing catch handling so the
spinner is always resolved in every path.

}
Loading