Paquetes npm con scope de ClawHub fallan al instalar con ENOENT
Plugins y skills publicados con nombres npm con scope (@scope/package) generan errores ENOENT durante la instalación porque la ruta del archivo temporal contiene segmentos de directorio sin resolver.
🔍 Síntomas
Manifestación principal del error
Al intentar instalar cualquier paquete npm con ámbito de ClawHub, se produce un error ENOENT durante la fase de escritura del archivo:
$ openclaw plugins install @axonflow/[email protected]
Resolving clawhub:@axonflow/[email protected]…
ClawHub code-plugin @axonflow/[email protected] channel=community verification=source-linked
Compatibility: pluginApi=>=2026.3.22 minGateway=2026.3.22
ClawHub package "@axonflow/openclaw" is community; review source and verification before enabling.
ENOENT: no such file or directory, open '/var/folders/ld/8b9xk7n52sg7q5vz7q1l8r840000gn/T/openclaw-clawhub-package-XXXXXX/@axonflow/openclaw.zip'
Observaciones de diagnóstico
- Paquetes sin ámbito funcionan correctamente: Los paquetes sin un segmento de ámbito se instalan sin errores
$ openclaw plugins install mywallet # Output: Successfully installed plugin "mywallet"- El error persiste en todas las versiones: Todos los patrones `@scope/name` activan el fallo independientemente del ámbito o nombre del paquete
- Código de salida: El proceso termina con el código de salida
ENOENT(equivalente numérico:34)
Detalles del objeto de error
El objeto de error de JavaScript proporciona estas propiedades:
{
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/var/folders/.../openclaw-clawhub-package-XXXXXX/@axonflow/openclaw.zip'
}La observación crítica es que la ruta contiene una barra diagonal en el segmento del nombre de archivo, lo que el resolvedor de rutas POSIX interpreta como un separador de directorios.
🧠 Causa raíz
Secuencia de fallo arquitectónico
La pipeline de instalación en dist/clawhub-CFvPS51z.js sigue esta secuencia:
- Crear un directorio temporal único usando
fs.mkdtemp() - Construir la ruta del archivo uniendo el directorio temporal con el nombre del paquete
- Escribir los bytes del zip descargado directamente en la ruta construida
Análisis de la ruta de código
El código problemático (instalación de plugins, línea aproximada 89):
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);El patrón idéntico existe en la ruta de instalación de habilidades (downloadClawHubSkillArchive, línea aproximada 232):
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);Desglose de la resolución de rutas
Cuando params.name es igual a @axonflow/openclaw:
| Paso | Operación | Resultado |
|---|---|---|
| 1 | path.join(tmpDir, "@axonflow/openclaw.zip") | <tmpDir>/@axonflow/openclaw.zip |
| 2 | path.dirname(archivePath) | <tmpDir>/@axonflow |
| 3 | fs.mkdtemp() crea | Solo <tmpDir> |
| 4 | fs.writeFile() intenta escribir | <tmpDir>/@axonflow/openclaw.zip |
| 5 | El directorio padre @axonflow no existe | ENOENT lanzado |
Por qué fs.mkdtemp no crea directorios intermedios
fs.mkdtemp(prefix) crea exactamente un directorio y devuelve su ruta absoluta. No:
- Analiza el prefijo en busca de segmentos de directorio
- Crea ningún subdirectorio implícito por la cadena del prefijo
- Valida que la ruta devuelta coincida con la estructura del prefijo
Por lo tanto, la llamada produce <tmpDir> (por ejemplo, /tmp/openclaw-clawhub-package-abc123), pero el código posteriormente intenta escribir en <tmpDir>/@axonflow/openclaw.zip, lo cual requiere un subdirectorio @axonflow/ que no existe.
Convención de ámbito de npm
La sintaxis @scope/name es la convención estándar de npm para paquetes con ámbito. ClawHub soporta este esquema de nomenclatura para identificadores de plugins y habilidades, lo que convierte este error en un bloqueador sistémico para todos los paquetes de la comunidad que usan esta convención.
🛠️ Solución paso a paso
Opción A: Sanitizar el nombre de archivo (Recomendado)
Reemplazar las barras diagonales en el nombre del paquete antes de construir la ruta del archivo. Esta es la solución más simple con un cambio mínimo de comportamiento.
Antes:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);Después:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const safeName = params.name.replace(/\//g, '_');
const archivePath = path.join(tmpDir, `${safeName}.zip`);
await fs.writeFile(archivePath, bytes);Esto convierte @axonflow/openclaw en @axonflow_openclaw, produciendo la ruta válida <tmpDir>/@axonflow_openclaw.zip.
Opción B: Asegurar que el directorio padre exista
Usar fs.mkdir con el indicador recursive para crear cualquier directorio padre necesario antes de escribir. Esto preserva la estructura completa de directorios si otro código depende de ella.
Antes:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);Después:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.mkdir(path.dirname(archivePath), { recursive: true });
await fs.writeFile(archivePath, bytes);Esto crea <tmpDir>/@axonflow/ como directorio, y luego escribe <tmpDir>/@axonflow/openclaw.zip.
Objetivos de aplicación
Aplica la solución en ambas ubicaciones:
- Instalación de plugins: Función que maneja
openclaw plugins install(alrededor de la línea 89) - Instalación de habilidades: Función
downloadClawHubSkillArchive(alrededor de la línea 232)
Compilación y despliegue
Después de modificar los archivos TypeScript/JavaScript de origen:
# Recompilar el módulo afectado
npm run build -- --filter=clawhub
# O recompilar todos los paquetes si el sistema de compilación lo requiere
npm run build
# Ejecutar las pruebas para verificar que no hay regresiones
npm test -- --grep "clawhub"🧪 Verificación
Pasos de verificación funcional
- Probar la instalación de plugins con ámbito:
$ openclaw plugins install @axonflow/[email protected]Resolving clawhub:@axonflow/[email protected]… ClawHub code-plugin @axonflow/[email protected] channel=community verification=source-linked Compatibility: pluginApi=>=2026.3.22 minGateway=>=2026.3.22 Downloading package archive… Extracting to plugins directory… Successfully installed plugin “@axonflow/openclaw” (version 1.2.1)
- Verificar que el plugin es reconocido:
$ openclaw plugins list | grep axonflow @axonflow/openclaw 1.2.1 enabled - Probar la instalación de habilidades con ámbito (si aplica):
$ openclaw skills install @company/[email protected] Successfully installed skill "@company/enterprise-skill" (version 2.0.0)
Pruebas de regresión
Confirmar que los paquetes sin ámbito continúan funcionando:
$ openclaw plugins install mywallet
$ openclaw plugins list | grep mywallet
mywallet 1.5.0 enabled
$ openclaw plugins install unpkg-test
$ openclaw plugins list | grep unpkg-test
unpkg-test 0.1.0 enabledVerificación del código de salida
$ openclaw plugins install @axonflow/[email protected]
$ echo $?
0Una instalación exitosa devuelve el código de salida 0. El ENOENT que fallaba anteriormente habría devuelto un código de salida distinto de cero.
Validación de pruebas unitarias
Si el código base incluye pruebas unitarias para la integración de ClawHub:
$ npm test -- --grep "downloadClawHubPlugin"
downloadClawHubPlugin
✓ should handle scoped package names (@scope/name)
✓ should handle unscoped package names
✓ should handle version specifiers
$ npm test -- --grep "downloadClawHubSkillArchive"
downloadClawHubSkillArchive
✓ should handle scoped skill names
✓ should handle nested scope names (@org/team/skill)⚠️ Errores comunes
Trampas específicas del entorno
- Contenedores Docker con tmpfs: Algunas configuraciones de Docker montan
/tmpcomo tmpfs con espacio limitado. Los paquetes con ámbito que tengan archivos zip grandes pueden fallar conENOSPCen lugar deENOENT. Verificar la asignación de tamaño de tmpfs:df -h /tmp # Asegurar espacio adecuado para los archivos de plugins (tmpfs por defecto a menudo 64MB) - Separadores de rutas en Windows: Aunque el error es técnicamente independiente de la plataforma, las rutas en Windows pueden presentar un comportamiento diferente con caracteres
@en las rutas. Evitar rutas que contengan@en contextos sensibles a seguridad. - Enlaces simbólicos en tmp: Si
os.tmpdir()se resuelve a una ruta de enlace simbólico (común en entornos de desarrollo), asegurar que el destino soporte la creación de directorios.
Configuraciones incorrectas del usuario
- Formato incorrecto del nombre del paquete: Los usuarios a veces omiten el ámbito cuando existe:
# Incorrecto openclaw plugins install axonflow/openclawCorrecto
openclaw plugins install @axonflow/openclaw
- Mayúsculas y minúsculas: Los ámbitos de npm son sensibles a mayúsculas.
@Axonflowy@axonflowson ámbitos distintos. - Versión fijada pero ámbito faltante: Al especificar versiones, asegurar que el identificador completo del paquete esté entre comillas para prevenir la interpretación del shell:
# Las comillas previenen problemas de expansión del shell openclaw plugins install "@axonflow/[email protected]"Sin comillas en algunos shells, @ puede activar expansión de variables
openclaw plugins install @axonflow/[email protected] # Puede fallar dependiendo del shell
Casos extremos
- Inyección de doble barra: Si
params.namecontiene barras diagonales al inicio o final, la regex de sanitización debe manejarlas. Extender la Opción A:const safeName = params.name.replace(/\//g, '_').replace(/^_+|_+$/g, ''); - Unicode y emoji en nombres de paquetes: Aunque no es práctica estándar de npm, algunos registros permiten caracteres no ASCII. Probar con nombres de paquetes internacionalizados si está soportado.
- Nombres de paquetes extremadamente largos: npm limita los nombres de paquetes a 214 caracteres. Los nombres con ámbito muy largos pueden acercarse a los límites de longitud de rutas del sistema de archivos en algunas plataformas (típicamente 255 bytes por componente de ruta).
Limpieza del directorio temporal
Los directorios temporales creados por fs.mkdtemp no se limpian automáticamente en caso de fallo del proceso o SIGKILL. Considerar añadir manejadores de limpieza:
process.on('SIGINT', () => {
fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
process.exit(1);
});🔗 Errores relacionados
Directamente relacionados
ENOENT: no such file or directory— El error canónico al intentar escribir en una ruta que no existe. Este problema es un caso específico donde la ruta contiene separadores de directorio inesperados.ENOTDIR: not a directory— Puede ocurrir si un componente de la ruta que debería ser un directorio se trata como un archivo, o viceversa. Relacionado cuando la sanitización del nombre de archivo crea conflictos inadvertidamente.EISDIR: is a directory— Podría ocurrir si la sanitización del nombre del paquete produce una cadena vacía, resultando en intentar escribir en el directorio temporal mismo.
Problemas históricos similares
- npm install con paquetes con ámbito en directorios de salida personalizados — Problemas similares de construcción de rutas afectaron a las primeras versiones del CLI de npm al especificar directorios personalizados
--prefixque contenían paquetes con ámbito. - Carga de artefactos de GitHub Actions con nombres con prefijo @ — El paso de carga de artefactos fallaba cuando los nombres de artefactos contenían caracteres
@, ya que el backend interpretaba@como una referencia a otro artefacto. - Nombre de archivo de chunk de Webpack con
[name]conteniendo barras diagonales — La salida de compilación fallaba al escribir chunks cuando el[name]resuelto contenía separadores de ruta, requiriendo la misma solución de sanitización.
Patrón de paquetes de ClawHub afectado
Este error impacting a un patrón de convención de nomenclatura específico en el ecosistema de ClawHub:
# Paquetes con ámbito (ROTO)
@axonflow/openclaw
@company/workflow-engine
@org/team/shared-utilities
# Paquetes sin ámbito (FUNCIONA)
mywallet
workflow-engine
shared-utilitiesCualquier paquete que use la convención estándar @scope/name de npm se ve afectado hasta que se despliegue la solución.
Monitoreo y detección
Para detectar este problema en logs de producción:
# Filtrar logs por el patrón ENOENT específico
grep "ENOENT.*openclaw-clawhub-package.*@.*\.zip" /var/log/openclaw/*.log
# Contar instalaciones afectadas por ventana de tiempo
grep "clawhub.*ENOENT" /var/log/openclaw/*.log | \
awk '{print $4}' | sort | uniq -c | sort -rn | head -20