From 2b0d627396cab955ea29c73bdffc1a5c10098a4d Mon Sep 17 00:00:00 2001 From: Pandipipas <62224708+Pandipipas@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:27:11 +0100 Subject: [PATCH] feat: complete pending roadmap items with doctor, hardening, and code quality --- .env.example | 15 + .prettierignore | 5 + .prettierrc | 5 + README.md | 87 +- docs/architecture.md | 24 + docs/refactor-roadmap.md | 23 + docs/troubleshooting.md | 27 + eslint.config.mjs | 30 + package-lock.json | 1176 ++++++++++++++++++++++++- package.json | 15 +- scripts/doctor.mjs | 98 +++ src/main/config/runtime-config.ts | 4 +- src/main/errors/error-presenter.ts | 9 +- src/main/errors/logger.ts | 34 + src/main/main.ts | 2 +- src/main/nodecg/process-manager.ts | 75 +- src/main/utils/timing.ts | 6 +- src/main/windows/window-factory.ts | 5 +- src/tests/navigation-security.test.ts | 10 +- src/tests/process-manager.test.ts | 76 +- 20 files changed, 1620 insertions(+), 106 deletions(-) create mode 100644 .env.example create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 docs/architecture.md create mode 100644 docs/troubleshooting.md create mode 100644 eslint.config.mjs create mode 100644 scripts/doctor.mjs create mode 100644 src/main/errors/logger.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ae9fbd4 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Runtime / app +SCOREKO_APP_TITLE=Scoreko +SCOREKO_APP_USER_MODEL_ID=com.scoreko.desktop +# SCOREKO_APP_ICON_PATH=static/icons/icon.ico + +# NodeCG +NODECG_BUNDLE_NAME=scoreko-dev +NODECG_PORT=9090 +SCOREKO_DASHBOARD_ROUTE=dashboard/scoreko-dev/main.html?standalone=true +SCOREKO_LOADING_ROUTE=dashboard/loading/main.html?standalone=true + +# Timing +ELECTRON_LOAD_DELAY_MS=10000 +NODECG_STARTUP_TIMEOUT_MS=30000 +NODECG_KILL_TIMEOUT_MS=2500 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4b56620 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +release +lib/nodecg +node_modules +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2dc4363 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": false, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/README.md b/README.md index f528794..de96c39 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Wrapper de Electron para empaquetar una instalación de NodeCG que incluya el bu ## Requisitos clave +- Node `>=22` (`.nvmrc` incluido). - Electron fijado en `39.5.1`. ## Qué hace @@ -33,81 +34,31 @@ scoreko-electron-dev/ - `npm run dev`: modo desarrollo. - `npm run build`: compila TypeScript y copia assets. - `npm run start`: build y ejecución local. +- `npm run test`: build + tests unitarios (`node:test`). +- `npm run doctor`: preflight de configuración y entorno (`lib/nodecg`, permisos, puerto y env vars). +- `npm run lint`: reglas mínimas de calidad con ESLint. +- `npm run format`: validación de formato con Prettier. - `npm run pack`: genera app sin instalador. - `npm run dist`: genera instalador. -- `npm run rebuild:native`: rebuild nativo auxiliar en `lib/nodecg`. -## Variables de entorno útiles +## Variables de entorno -- `NODECG_BUNDLE_NAME` (default: `scoreko-dev`) -- `NODECG_PORT` (default: `9090`) -- `SCOREKO_DASHBOARD_ROUTE` (default: `dashboard/example/main.html?standalone=true`) -- `SCOREKO_LOADING_ROUTE` (default: `dashboard/loading/main.html?standalone=true`) -- `SCOREKO_APP_TITLE` (default: `Scoreko`) -- `SCOREKO_APP_USER_MODEL_ID` (default: `com.scoreko.desktop`) -- `SCOREKO_APP_ICON_PATH` (default: búsqueda automática en `static/icons/icon.ico` y `static/icons/icon.png`) -- `ELECTRON_LOAD_DELAY_MS` (default: `10000`) -- `NODECG_STARTUP_TIMEOUT_MS` (default: `30000`) -- `NODECG_KILL_TIMEOUT_MS` (default: `2500`) +La **fuente única de defaults** está en `.env.example`. -## Assets de íconos incluidos +1. Copia `.env.example` a `.env` (o exporta variables en tu shell/CI). +2. Ajusta sólo lo necesario. +3. Ejecuta `npm run doctor` para validar configuración antes de arrancar. -Se incluye `static/icons/` con placeholder editable: +## Build multi-plataforma (iconos) -- `static/icons/icon.svg` +- `build.win.icon`: `static/icons/icon.ico` +- `build.linux.icon`: `static/icons` +- `build.mac.icon`: `static/icons/icon.icns` -> En este repo no se versionan binarios. Para distribución final agrega localmente `static/icons/icon.ico` (Windows) y/o `static/icons/icon.png` (Linux/macOS runtime). +> El `.icns` se referencia en la configuración de build y debe existir localmente para empaquetar macOS. -## Personalización (Windows / Electron) +## Troubleshooting y arquitectura -### 1) Título de la app - -- **Runtime (ventanas):** `SCOREKO_APP_TITLE`. -- **Build (instalador y ejecutable):** `build.productName`. - -### 2) Ícono en barra de tareas y esquina superior - -Electron toma automáticamente el primer archivo existente en este orden: - -1. `SCOREKO_APP_ICON_PATH` (si lo defines) -2. `static/icons/icon.ico` -3. `static/icons/icon.png` -4. `static/icon.ico` -5. `static/icon.png` - -### 3) Ícono del `.exe` y accesos directos - -Quedó configurado en `package.json` con `build.win.icon` + `build.nsis.installerIcon`/`uninstallerIcon`. - -### 4) Nombre/autor del popup de Firewall de Windows - -Ese diálogo usa metadata del ejecutable firmado: - -- Nombre de app: normalmente `build.productName` y metadata del binario. -- Publisher/autor: certificado de firma de código (si no firmas puede verse `Unknown publisher`). - -Campos clave a revisar: - -- `description` -- `author` -- `build.productName` -- `build.win.executableName` -- `build.appId` - -Además, firma el `.exe` con certificado (`CSC_LINK` / `CSC_KEY_PASSWORD` en `electron-builder`). - -## Checklist de personalización extra - -- `build.appId` -- `build.artifactName` -- `build.win.executableName` -- `build.win.icon` -- `build.nsis` (nombre del setup, íconos, shortcut) -- `build.mac.icon` -- `build.linux.icon` -- `SCOREKO_APP_USER_MODEL_ID` -- `SCOREKO_APP_ICON_PATH` - -## Roadmap de refactor recomendado - -Si quieres una propuesta detallada de refactorización, limpieza y mejoras sin romper funcionalidad, revisa: `docs/refactor-roadmap.md`. +- Guía de troubleshooting: `docs/troubleshooting.md` +- Mapa de arquitectura: `docs/architecture.md` +- Roadmap: `docs/refactor-roadmap.md` diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..b716afd --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,24 @@ +# Arquitectura del proceso principal + +## Flujo de arranque + +1. `src/main/main.ts` carga `appConfig` desde `config/runtime-config.ts`. +2. Crea ventanas (`windows/window-factory.ts`). +3. Arranca NodeCG con `nodecg/process-manager.ts`. +4. Espera readiness HTTP y muestra loading -> dashboard principal. +5. En cierre, ejecuta un único flujo de stop graceful para evitar procesos huérfanos. + +## Módulos principales + +- `config/runtime-config.ts`: lectura/validación de env vars. +- `nodecg/process-manager.ts`: start, readiness y stop de NodeCG, validaciones de instalación/permisos/puerto. +- `windows/window-factory.ts`: creación de ventanas y política de navegación. +- `windows/navigation-security.ts`: allowlist de navegación interna y esquemas externos seguros. +- `errors/error-presenter.ts`: presentación de errores fatales. +- `errors/logger.ts`: logging estructurado (`info/warn/error/debug`). + +## Principios + +- Refactors mecánicos primero. +- Hardening incremental con fallback conservador. +- Validación automática por `typecheck`, `build`, `test`, `doctor`, `lint`. diff --git a/docs/refactor-roadmap.md b/docs/refactor-roadmap.md index c25d713..c15445a 100644 --- a/docs/refactor-roadmap.md +++ b/docs/refactor-roadmap.md @@ -5,6 +5,7 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio ## 1) Arquitectura y organización del código ### 1.1 Separar `main.ts` por responsabilidades + - **Problema actual:** `src/main/main.ts` concentra configuración, UI, lifecycle, gestión de procesos, manejo de errores y utilidades. - **Acciones:** - Crear `src/main/config/runtime-config.ts` para parseo de env vars. @@ -15,11 +16,13 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio - **Implementación segura:** mover funciones sin cambiar lógica, cubrir con tests unitarios antes de modificar comportamiento. ### 1.2 Consolidar constantes de runtime + - **Problema actual:** constantes de URLs y tamaños están dispersas. - **Acciones:** agrupar en `src/main/constants.ts` y tiparlas con `as const`. - **Beneficio:** facilita ajuste de defaults y revisiones. ### 1.3 Normalizar naming interno + - **Mejoras propuestas:** - `runtimeConfig` → `appConfig`. - `nodecgPath` → `nodecgRootPath`. @@ -29,6 +32,7 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio ## 2) Robustez de proceso NodeCG ### 2.1 Endurecer validaciones de instalación + - **Mejoras:** - Validar permisos de lectura/escritura en `lib/nodecg`. - Validar si el puerto está ocupado antes de lanzar. @@ -36,6 +40,7 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio - **Implementación segura:** agregar validaciones opt-in primero con logs, luego convertir a hard-fail. ### 2.2 Mejorar el check de “ready” + - **Problema:** readiness actual con `GET /` + `response.ok || 404` puede dar falsos positivos. - **Acciones:** - Intentar endpoint más explícito de NodeCG si existe. @@ -43,6 +48,7 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio - **Compatibilidad:** mantener fallback al check actual inicialmente. ### 2.3 Control del shutdown más determinista + - **Acciones:** - Añadir estado explícito (`starting/running/stopping/stopped`). - Evitar dobles señales en hooks `before-quit`, `will-quit`, `process.exit`. @@ -51,41 +57,48 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio ## 3) Ventanas Electron y UX de carga ### 3.1 Rework de loading flow + - **Mejoras:** - Evitar cargar la ventana principal demasiado pronto si falla dashboard. - Añadir timeout específico para `mainWindow.loadURL`. - Incluir fallback de pantalla de error amigable dentro de Electron. ### 3.2 Seguridad de navegación + - **Acciones:** - Validar que `setWindowOpenHandler` y `will-navigate` permitan solo dominio esperado (`localhost:PORT`). - Rechazar esquemas inseguros (`file:`, `javascript:`). - **Beneficio:** hardening del proceso principal. ### 3.3 Ajustes de resolución + - **Mejora:** no fijar `minWidth/minHeight` a 1920x1080 en todos los escenarios. - **Propuesta segura:** usar valores por env var con defaults actuales para mantener compatibilidad. ## 4) Configuración y variables de entorno ### 4.1 Validación tipada de env vars + - **Acciones:** - Introducir esquema (`zod` o validación manual centralizada). - Rechazar puertos fuera de rango. - Marcar strings vacíos inválidos en rutas críticas. ### 4.2 Documentación ejecutable + - **Acciones:** - Añadir `.env.example` con todos los defaults. - Script de validación `npm run doctor` para detectar configuración inválida. ### 4.3 Unificar defaults entre código y README + - **Problema:** existe posible drift entre doc y runtime. - **Acción:** generar bloque de README automáticamente desde una fuente única de config. ## 5) Build, packaging y distribución ### 5.1 Revisar iconos por plataforma + - **Problema:** en `build.mac.icon` se usa `.ico` (no ideal para macOS). - **Acciones:** - Usar `.icns` en macOS. @@ -93,11 +106,13 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio - **Estrategia segura:** fallback conservando lo actual hasta tener assets definitivos. ### 5.2 Pipeline reproducible + - **Acciones:** - Asegurar lockfile limpio y versión de Node fijada con `.nvmrc`. - Añadir CI mínima (`typecheck`, build, smoke test). ### 5.3 Reducción de tamaño de artefacto + - **Acciones:** - Revisar qué se copia en `extraResources`. - Excluir archivos no necesarios (logs, tests, cachés) en empaquetado. @@ -105,27 +120,32 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio ## 6) Calidad de código y testing ### 6.1 Añadir linting/formatting + - **Acciones:** - Configurar ESLint + Prettier. - Reglas mínimas: imports ordenados, no variables no usadas, complejidad controlada. ### 6.2 Unit tests para utilidades críticas + - **Cobertura objetivo inicial:** - `parseEnvInt`, `getEnv`, `getOptionalEnv`. - Resolución de icon path. - Cálculo de delays y timeouts. ### 6.3 Integration smoke tests + - **Acción:** test que verifique arranque controlado de Electron main con mocks (sin UI real). - **Objetivo:** detectar regresiones de lifecycle y cierre de NodeCG. ## 7) Observabilidad y diagnóstico ### 7.1 Logger estructurado + - **Acción:** reemplazar `console.log` por logger con niveles (`info/warn/error/debug`) y contexto. - **Beneficio:** depuración más rápida en producción. ### 7.2 Error codes y troubleshooting + - **Acciones:** - Estandarizar errores con códigos (`E_NODECG_NOT_FOUND`, etc.). - Añadir sección “Troubleshooting” en README con causas/soluciones. @@ -133,13 +153,16 @@ Este documento detalla una propuesta de mejoras para `scoreko-electron-dev` prio ## 8) Limpieza técnica (deuda) ### 8.1 Eliminar lógica duplicada de cierre + - **Problema:** varios handlers llaman `stopNodeCG()`. - **Acción:** centralizar estrategia de parada en un solo coordinador. ### 8.2 Extraer utilidades de proceso SO + - **Acción:** separar lógica Windows (`taskkill`) y POSIX (`process.kill`) en módulos específicos. ### 8.3 Revisar `shell: true` en spawn + - **Motivo:** reducir superficie y comportamiento inesperado. - **Plan seguro:** introducir feature-flag para comparar comportamiento antes de retirar. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..866587b --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,27 @@ +# Troubleshooting + +## `No existe la carpeta NodeCG` + +- Verifica que exista `lib/nodecg`. +- Asegúrate de que el proyecto contiene una instalación completa de NodeCG. + +## `Sin permisos de lectura/escritura sobre NodeCG` + +- Ajusta permisos de `lib/nodecg` para el usuario que ejecuta Electron. +- En Linux/macOS: `chmod -R u+rw lib/nodecg` (según tu política local). + +## `El puerto ya está en uso` + +- Libera el puerto o define `NODECG_PORT` en `.env`. +- Usa `npm run doctor` para validar disponibilidad antes de arrancar. + +## `Timeout esperando NodeCG` + +- Revisa logs de NodeCG en la salida estándar. +- Incrementa `NODECG_STARTUP_TIMEOUT_MS` si el entorno es lento. +- Verifica dependencias de NodeCG (`cd lib/nodecg && npm install`). + +## Build macOS falla por icono + +- La configuración espera `static/icons/icon.icns`. +- Crea ese archivo antes de ejecutar empaquetado para macOS. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..3eae9e9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,30 @@ +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; + +export default [ + { + ignores: ["dist/**", "release/**", "lib/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + parserOptions: { + project: "./tsconfig.json", + }, + }, + plugins: { + "@typescript-eslint": tseslint, + }, + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + }, + }, + { + files: ["**/*.js", "**/*.mjs"], + rules: { + "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 78be278..3c7e8d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,13 @@ "devDependencies": { "@electron/rebuild": "^3.7.1", "@types/node": "^22.10.5", + "@typescript-eslint/eslint-plugin": "^8.22.0", + "@typescript-eslint/parser": "^8.22.0", "concurrently": "^9.1.2", "electron": "39.5.1", "electron-builder": "^25.1.8", + "eslint": "^9.19.0", + "prettier": "^3.4.2", "rimraf": "^6.0.1", "typescript": "^5.7.3", "wait-on": "^8.0.1" @@ -365,6 +369,209 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -426,6 +633,58 @@ "@hapi/hoek": "^11.0.2" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", @@ -667,6 +926,13 @@ "@types/ms": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -684,6 +950,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -752,6 +1025,272 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -776,6 +1315,29 @@ "dev": true, "license": "ISC" }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1502,6 +2064,16 @@ "node": ">= 0.4" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2083,6 +2655,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -2526,7 +3105,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, @@ -2534,6 +3112,227 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -2587,6 +3386,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -2597,6 +3403,37 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -2637,6 +3474,44 @@ "node": ">=10" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -2865,6 +3740,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/glob/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2914,6 +3802,19 @@ "node": ">=10.0" } }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -3175,6 +4076,33 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3244,6 +4172,16 @@ "is-ci": "bin.js" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3254,6 +4192,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -3392,6 +4343,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3493,6 +4451,36 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -3532,6 +4520,13 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -3883,6 +4878,13 @@ "dev": true, "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -4114,6 +5116,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -4164,6 +5184,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4187,6 +5223,29 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4270,6 +5329,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -4285,6 +5357,32 @@ "node": ">=10.4.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proc-log": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", @@ -4482,6 +5580,16 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -4962,6 +6070,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -5046,6 +6167,23 @@ "fs-extra": "^10.0.0" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -5086,6 +6224,19 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5093,6 +6244,19 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -5260,6 +6424,16 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 46bd4e4..bb8dabe 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,12 @@ "dist": "npm run build && electron-builder", "rebuild:native": "node scripts/rebuild-nodecg-native.mjs", "rebuild:better-sqlite3": "electron-rebuild --version 39.5.1 --module-dir lib/nodecg/workspaces/database-adapter-sqlite-legacy --only better-sqlite3 -f", - "test": "npm run build && node --test dist/tests/**/*.test.js" + "test": "npm run build && node --test dist/tests/**/*.test.js", + "doctor": "node scripts/doctor.mjs", + "lint": "eslint . --ext .ts,.js,.mjs", + "lint:fix": "npm run lint -- --fix", + "format": "prettier --check .", + "format:write": "prettier --write ." }, "build": { "appId": "com.scoreko.desktop", @@ -49,7 +54,7 @@ "target": [ "dmg" ], - "icon": "static/icons/icon.png" + "icon": "static/icons/icon.icns" }, "linux": { "target": [ @@ -87,6 +92,10 @@ "electron-builder": "^25.1.8", "rimraf": "^6.0.1", "typescript": "^5.7.3", - "wait-on": "^8.0.1" + "wait-on": "^8.0.1", + "eslint": "^9.19.0", + "@typescript-eslint/parser": "^8.22.0", + "@typescript-eslint/eslint-plugin": "^8.22.0", + "prettier": "^3.4.2" } } diff --git a/scripts/doctor.mjs b/scripts/doctor.mjs new file mode 100644 index 0000000..7cf3ed0 --- /dev/null +++ b/scripts/doctor.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import net from "node:net"; +import path from "node:path"; + +const cwd = process.cwd(); +const nodecgRootPath = path.resolve(cwd, "lib", "nodecg"); + +const checks = []; + +function addCheck(ok, title, details) { + checks.push({ ok, title, details }); +} + +function parsePort(name, fallback) { + const raw = process.env[name] ?? fallback; + const parsed = Number.parseInt(raw, 10); + if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) { + addCheck(false, `${name} inválido`, `Debe ser un entero entre 1 y 65535. Valor recibido: '${raw}'.`); + return null; + } + + addCheck(true, `${name} válido`, `${parsed}`); + return parsed; +} + +function parseIntInRange(name, fallback, min, max) { + const raw = process.env[name] ?? String(fallback); + const parsed = Number.parseInt(raw, 10); + if (!Number.isFinite(parsed) || parsed < min || parsed > max) { + addCheck(false, `${name} inválido`, `Debe ser un entero entre ${min} y ${max}. Valor recibido: '${raw}'.`); + return; + } + + addCheck(true, `${name} válido`, `${parsed}`); +} + +function checkNodecgInstall() { + const indexPath = path.join(nodecgRootPath, "index.js"); + const bundleName = (process.env.NODECG_BUNDLE_NAME ?? "scoreko-dev").trim(); + const bundlePath = path.join(nodecgRootPath, "bundles", bundleName); + + addCheck(fs.existsSync(nodecgRootPath), "NodeCG root", nodecgRootPath); + addCheck(fs.existsSync(indexPath), "NodeCG index.js", indexPath); + addCheck(fs.existsSync(bundlePath), `Bundle '${bundleName}'`, bundlePath); + + try { + fs.accessSync(nodecgRootPath, fs.constants.R_OK | fs.constants.W_OK); + addCheck(true, "Permisos lib/nodecg", "Lectura/escritura OK"); + } catch { + addCheck(false, "Permisos lib/nodecg", "Sin permisos de lectura/escritura en lib/nodecg"); + } +} + +function checkPortAvailability(port) { + return new Promise((resolve) => { + const server = net.createServer(); + + server.once("error", () => { + addCheck(false, `Puerto ${port}`, "Está ocupado. Libéralo o cambia NODECG_PORT."); + resolve(); + }); + + server.listen(port, "127.0.0.1", () => { + server.close(() => { + addCheck(true, `Puerto ${port}`, "Disponible"); + resolve(); + }); + }); + }); +} + +async function main() { + const port = parsePort("NODECG_PORT", "9090"); + parseIntInRange("ELECTRON_LOAD_DELAY_MS", 10000, 0, 600000); + parseIntInRange("NODECG_STARTUP_TIMEOUT_MS", 30000, 1000, 600000); + parseIntInRange("NODECG_KILL_TIMEOUT_MS", 2500, 0, 120000); + checkNodecgInstall(); + + if (port) { + await checkPortAvailability(port); + } + + for (const check of checks) { + const icon = check.ok ? "✅" : "❌"; + console.log(`${icon} ${check.title}: ${check.details}`); + } + + const hasFailures = checks.some((check) => !check.ok); + if (hasFailures) { + process.exitCode = 1; + return; + } + + console.log("\nDoctor finalizado: configuración válida."); +} + +main(); diff --git a/src/main/config/runtime-config.ts b/src/main/config/runtime-config.ts index 3c0512c..b9f09d4 100644 --- a/src/main/config/runtime-config.ts +++ b/src/main/config/runtime-config.ts @@ -67,7 +67,9 @@ export function parseEnvPort(name: string, fallback: string): string { const parsedValue = Number.parseInt(rawValue, 10); if (!Number.isFinite(parsedValue) || parsedValue < MIN_TCP_PORT || parsedValue > MAX_TCP_PORT) { - throw new Error(`La variable ${name} debe ser un puerto TCP válido (${MIN_TCP_PORT}-${MAX_TCP_PORT}). Valor recibido: '${rawValue}'.`); + throw new Error( + `La variable ${name} debe ser un puerto TCP válido (${MIN_TCP_PORT}-${MAX_TCP_PORT}). Valor recibido: '${rawValue}'.`, + ); } return String(parsedValue); diff --git a/src/main/errors/error-presenter.ts b/src/main/errors/error-presenter.ts index 6cc84e0..9245bb1 100644 --- a/src/main/errors/error-presenter.ts +++ b/src/main/errors/error-presenter.ts @@ -1,7 +1,9 @@ import { app, dialog } from "electron"; +import { logger } from "./logger"; + export function log(...args: unknown[]): void { - console.log("[scoreko-electron]", ...args); + logger.info("runtime", { args }); } export function formatErrorMessage(error: unknown): string { @@ -17,7 +19,10 @@ export function showFatalError(message: string, error?: unknown): void { const formattedError = error ? formatErrorMessage(error) : undefined; const details = formattedError ? `${message}\n\n${formattedError}` : message; - console.error(details); + logger.error("fatal-startup-error", { + message, + error: formattedError, + }); if (!app.isReady()) { return; diff --git a/src/main/errors/logger.ts b/src/main/errors/logger.ts new file mode 100644 index 0000000..dce9c65 --- /dev/null +++ b/src/main/errors/logger.ts @@ -0,0 +1,34 @@ +export type LogLevel = "debug" | "info" | "warn" | "error"; + +type LogContext = Record; + +function write(level: LogLevel, message: string, context?: LogContext): void { + const payload = { + ts: new Date().toISOString(), + level, + source: "scoreko-electron", + message, + ...(context ? { context } : {}), + }; + + const line = JSON.stringify(payload); + + if (level === "error") { + console.error(line); + return; + } + + if (level === "warn") { + console.warn(line); + return; + } + + console.log(line); +} + +export const logger = { + debug: (message: string, context?: LogContext): void => write("debug", message, context), + info: (message: string, context?: LogContext): void => write("info", message, context), + warn: (message: string, context?: LogContext): void => write("warn", message, context), + error: (message: string, context?: LogContext): void => write("error", message, context), +}; diff --git a/src/main/main.ts b/src/main/main.ts index f305694..b424ca8 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -34,7 +34,7 @@ async function launchApplication(): Promise { mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl }); loadingWindow = createLoadingWindow({ appConfig, rootPath }); - nodecgManager.startNodecgProcess(); + await nodecgManager.startNodecgProcess(); await nodecgManager.waitForNodecgReady(Date.now()); diff --git a/src/main/nodecg/process-manager.ts b/src/main/nodecg/process-manager.ts index a95e02d..7663a19 100644 --- a/src/main/nodecg/process-manager.ts +++ b/src/main/nodecg/process-manager.ts @@ -1,5 +1,6 @@ import { ChildProcess, spawn, SpawnOptions } from "node:child_process"; import fs from "node:fs"; +import net from "node:net"; import path from "node:path"; import { AppRuntimeConfig } from "../config/runtime-config"; @@ -25,10 +26,12 @@ type NodecgProcessManagerDeps = { setTimer: (handler: () => void, timeoutMs: number) => unknown; stdoutWrite: (chunk: string) => void; stderrWrite: (chunk: string) => void; + probePortAvailable: (port: number) => Promise; + hasReadWriteAccess: (candidatePath: string) => boolean; }; export type NodecgProcessManager = { - startNodecgProcess: () => ChildProcess; + startNodecgProcess: () => Promise; waitForNodecgReady: (startTime: number) => Promise; stopNodecgProcessGracefully: () => Promise; getProcess: () => ChildProcess | null; @@ -47,8 +50,21 @@ export function createNodecgProcessManager({ let nodecgProcess: ChildProcess | null = null; let stopNodecgPromise: Promise | null = null; - const startNodecgProcess = (): ChildProcess => { - validateNodecgInstall(nodecgRootPath, appConfig.bundleName, resolvedDeps.pathExists); + const startNodecgProcess = async (): Promise => { + validateNodecgInstall( + nodecgRootPath, + appConfig.bundleName, + resolvedDeps.pathExists, + resolvedDeps.hasReadWriteAccess, + ); + + const portAsNumber = Number.parseInt(appConfig.nodecgPort, 10); + const isPortAvailable = await resolvedDeps.probePortAvailable(portAsNumber); + if (!isPortAvailable) { + throw new Error( + `El puerto ${appConfig.nodecgPort} ya está en uso. Cierra el proceso que lo ocupa o configura NODECG_PORT antes de iniciar.`, + ); + } const indexPath = path.join(nodecgRootPath, "index.js"); const child = resolvedDeps.spawnProcess(resolvedDeps.execPath, [indexPath], { @@ -138,12 +154,15 @@ export function createNodecgProcessManager({ complete(); }); - resolvedDeps.setTimer(() => { - if (processToStop.exitCode === null && processToStop.signalCode === null) { - log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`); - killNodecgProcessTree(pid, "SIGKILL", log, resolvedDeps); - } - }, Math.max(0, appConfig.nodecgKillTimeoutMs)); + resolvedDeps.setTimer( + () => { + if (processToStop.exitCode === null && processToStop.signalCode === null) { + log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`); + killNodecgProcessTree(pid, "SIGKILL", log, resolvedDeps); + } + }, + Math.max(0, appConfig.nodecgKillTimeoutMs), + ); }); return stopNodecgPromise; @@ -169,10 +188,17 @@ function resolveDeps(deps?: Partial): NodecgProcessMan setTimer: deps?.setTimer ?? setTimeout, stdoutWrite: deps?.stdoutWrite ?? ((chunk) => process.stdout.write(chunk)), stderrWrite: deps?.stderrWrite ?? ((chunk) => process.stderr.write(chunk)), + probePortAvailable: deps?.probePortAvailable ?? probePortAvailable, + hasReadWriteAccess: deps?.hasReadWriteAccess ?? hasReadWriteAccess, }; } -function validateNodecgInstall(nodecgRootPath: string, bundleName: string, pathExists: (candidatePath: string) => boolean): void { +function validateNodecgInstall( + nodecgRootPath: string, + bundleName: string, + pathExists: (candidatePath: string) => boolean, + hasReadWriteAccessToPath: (candidatePath: string) => boolean, +): void { const indexPath = path.join(nodecgRootPath, "index.js"); const nodecgBootstrapPath = path.join(nodecgRootPath, "node_modules", "nodecg", "dist", "server", "bootstrap.js"); const bundlePath = path.join(nodecgRootPath, "bundles", bundleName); @@ -181,6 +207,10 @@ function validateNodecgInstall(nodecgRootPath: string, bundleName: string, pathE throw new Error(`No existe la carpeta NodeCG: ${nodecgRootPath}`); } + if (!hasReadWriteAccessToPath(nodecgRootPath)) { + throw new Error(`Sin permisos de lectura/escritura sobre NodeCG: ${nodecgRootPath}`); + } + if (!pathExists(indexPath)) { throw new Error(`No se encontró ${indexPath}. Copia una instalación completa de NodeCG en lib/nodecg.`); } @@ -207,6 +237,31 @@ function validateNodecgInstall(nodecgRootPath: string, bundleName: string, pathE } } +function hasReadWriteAccess(candidatePath: string): boolean { + try { + fs.accessSync(candidatePath, fs.constants.R_OK | fs.constants.W_OK); + return true; + } catch { + return false; + } +} + +function probePortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + + server.once("error", () => { + resolve(false); + }); + + server.listen(port, "127.0.0.1", () => { + server.close(() => { + resolve(true); + }); + }); + }); +} + function killNodecgProcessTree( pid: number, signal: NodeJS.Signals, diff --git a/src/main/utils/timing.ts b/src/main/utils/timing.ts index e71815b..baf6fa3 100644 --- a/src/main/utils/timing.ts +++ b/src/main/utils/timing.ts @@ -1,3 +1,7 @@ -export function getRemainingDelayMs(targetDelayMs: number, startedAtMs: number, currentTimeMs: number = Date.now()): number { +export function getRemainingDelayMs( + targetDelayMs: number, + startedAtMs: number, + currentTimeMs: number = Date.now(), +): number { return Math.max(0, targetDelayMs - (currentTimeMs - startedAtMs)); } diff --git a/src/main/windows/window-factory.ts b/src/main/windows/window-factory.ts index 8731dc7..565fb45 100644 --- a/src/main/windows/window-factory.ts +++ b/src/main/windows/window-factory.ts @@ -43,7 +43,10 @@ export function createMainWindow({ appConfig, rootPath, mainDashboardUrl }: Wind return window; } -export function createLoadingWindow({ appConfig, rootPath }: Omit): BrowserWindow { +export function createLoadingWindow({ + appConfig, + rootPath, +}: Omit): BrowserWindow { const window = new BrowserWindow(createWindowOptions({ appConfig, rootPath, isLoadingWindow: true })); window.on("page-title-updated", (event) => { diff --git a/src/tests/navigation-security.test.ts b/src/tests/navigation-security.test.ts index 0f2ec39..229ca40 100644 --- a/src/tests/navigation-security.test.ts +++ b/src/tests/navigation-security.test.ts @@ -13,11 +13,17 @@ test("shouldAllowInternalNavigation permite navegación interna esperada", () => }); test("shouldAllowInternalNavigation rechaza host no permitido", () => { - assert.equal(shouldAllowInternalNavigation("http://evil.local:9090/bundles/scoreko-dev/dashboard/page.html", dashboardUrl), false); + assert.equal( + shouldAllowInternalNavigation("http://evil.local:9090/bundles/scoreko-dev/dashboard/page.html", dashboardUrl), + false, + ); }); test("shouldAllowInternalNavigation rechaza puerto distinto", () => { - assert.equal(shouldAllowInternalNavigation("http://localhost:8080/bundles/scoreko-dev/dashboard/page.html", dashboardUrl), false); + assert.equal( + shouldAllowInternalNavigation("http://localhost:8080/bundles/scoreko-dev/dashboard/page.html", dashboardUrl), + false, + ); }); test("shouldAllowInternalNavigation rechaza esquemas inseguros", () => { diff --git a/src/tests/process-manager.test.ts b/src/tests/process-manager.test.ts index 5bf127b..7e88a5b 100644 --- a/src/tests/process-manager.test.ts +++ b/src/tests/process-manager.test.ts @@ -31,7 +31,7 @@ function getBaseConfig(): AppRuntimeConfig { }; } -test("startNodeCG valida instalación de NodeCG antes de arrancar", () => { +test("startNodeCG valida instalación de NodeCG antes de arrancar", async () => { const manager = createNodecgProcessManager({ isDev: true, nodecgRootPath: "/fake/nodecg", @@ -46,11 +46,29 @@ test("startNodeCG valida instalación de NodeCG antes de arrancar", () => { }, }); - assert.throws(() => { - manager.startNodecgProcess(); + await assert.rejects(async () => { + await manager.startNodecgProcess(); }, /No existe la carpeta NodeCG/); }); +test("startNodeCG falla si no hay permisos de lectura/escritura", async () => { + const manager = createNodecgProcessManager({ + isDev: true, + nodecgRootPath: "/fake/nodecg", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: getBaseConfig(), + log: () => undefined, + deps: { + pathExists: () => true, + hasReadWriteAccess: () => false, + }, + }); + + await assert.rejects(async () => { + await manager.startNodecgProcess(); + }, /Sin permisos de lectura\/escritura/); +}); + test("waitForNodeCGReady resuelve cuando el endpoint responde 404", async () => { const child = new MockChildProcess(4321); const manager = createNodecgProcessManager({ @@ -62,17 +80,19 @@ test("waitForNodeCGReady resuelve cuando el endpoint responde 404", async () => deps: { pathExists: () => true, spawnProcess: () => child as unknown as import("node:child_process").ChildProcess, - fetchUrl: async () => ({ ok: false, status: 404 } as Response), - setTimer: ((handler: (...args: unknown[]) => void, _timeoutMs: number) => { + fetchUrl: async () => ({ ok: false, status: 404 }) as Response, + setTimer: (handler: (...args: unknown[]) => void, _timeoutMs: number) => { handler(); return 0 as never; - }), + }, stdoutWrite: () => undefined, stderrWrite: () => undefined, + probePortAvailable: async () => true, + hasReadWriteAccess: () => true, }, }); - manager.startNodecgProcess(); + await manager.startNodecgProcess(); await assert.doesNotReject(async () => { await manager.waitForNodecgReady(Date.now()); }); @@ -92,20 +112,22 @@ test("stopNodeCG envía SIGTERM y luego SIGKILL si el proceso no sale", async () deps: { pathExists: () => true, spawnProcess: () => child as unknown as import("node:child_process").ChildProcess, - fetchUrl: async () => ({ ok: false, status: 404 } as Response), + fetchUrl: async () => ({ ok: false, status: 404 }) as Response, killProcess: (pid, signal) => { killSignals.push({ pid, signal }); }, - setTimer: ((handler: (...args: unknown[]) => void, _timeoutMs: number) => { + setTimer: (handler: (...args: unknown[]) => void, _timeoutMs: number) => { timers.push(() => handler()); return 0 as never; - }), + }, stdoutWrite: () => undefined, stderrWrite: () => undefined, + probePortAvailable: async () => true, + hasReadWriteAccess: () => true, }, }); - manager.startNodecgProcess(); + await manager.startNodecgProcess(); const stopPromise = manager.stopNodecgProcessGracefully(); assert.deepEqual(killSignals, [{ pid: -9999, signal: "SIGTERM" }]); @@ -123,7 +145,6 @@ test("stopNodeCG envía SIGTERM y luego SIGKILL si el proceso no sale", async () await stopPromise; }); - test("stopNodeCG reutiliza la misma promesa cuando se invoca en paralelo", async () => { const child = new MockChildProcess(5555); @@ -136,15 +157,17 @@ test("stopNodeCG reutiliza la misma promesa cuando se invoca en paralelo", async deps: { pathExists: () => true, spawnProcess: () => child as unknown as import("node:child_process").ChildProcess, - fetchUrl: async () => ({ ok: false, status: 404 } as Response), + fetchUrl: async () => ({ ok: false, status: 404 }) as Response, killProcess: () => undefined, setTimer: () => 0, stdoutWrite: () => undefined, stderrWrite: () => undefined, + probePortAvailable: async () => true, + hasReadWriteAccess: () => true, }, }); - manager.startNodecgProcess(); + await manager.startNodecgProcess(); const firstStop = manager.stopNodecgProcessGracefully(); const secondStop = manager.stopNodecgProcessGracefully(); @@ -170,7 +193,7 @@ test("stopNodeCG normaliza timeout negativo a cero", async () => { deps: { pathExists: () => true, spawnProcess: () => child as unknown as import("node:child_process").ChildProcess, - fetchUrl: async () => ({ ok: false, status: 404 } as Response), + fetchUrl: async () => ({ ok: false, status: 404 }) as Response, killProcess: () => undefined, setTimer: (handler, timeoutMs) => { timeouts.push(timeoutMs); @@ -179,10 +202,12 @@ test("stopNodeCG normaliza timeout negativo a cero", async () => { }, stdoutWrite: () => undefined, stderrWrite: () => undefined, + probePortAvailable: async () => true, + hasReadWriteAccess: () => true, }, }); - manager.startNodecgProcess(); + await manager.startNodecgProcess(); const stopPromise = manager.stopNodecgProcessGracefully(); assert.ok(timeouts.includes(0)); @@ -190,3 +215,22 @@ test("stopNodeCG normaliza timeout negativo a cero", async () => { child.emit("exit", 0, null); await stopPromise; }); + +test("startNodeCG falla si el puerto ya está ocupado", async () => { + const manager = createNodecgProcessManager({ + isDev: true, + nodecgRootPath: "/fake/nodecg", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: getBaseConfig(), + log: () => undefined, + deps: { + pathExists: () => true, + hasReadWriteAccess: () => true, + probePortAvailable: async () => false, + }, + }); + + await assert.rejects(async () => { + await manager.startNodecgProcess(); + }, /ya está en uso/); +});