diff --git a/.env.example b/.env.example index c0cff3e..993a51a 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,8 @@ REMOVE_QUALITY_BLOCKED=false BLOCK_REMOVED_QUALITY_RELEASES=false REMOVE_ARCHIVE_BLOCKED=false BLOCK_REMOVED_ARCHIVE_RELEASES=false +REMOVE_NO_FILES_RELEASES=false +BLOCK_REMOVED_NO_FILES_RELEASES=false # Schedule (cron format) SCHEDULE=*/5 * * * * diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9601b93 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 27a7388..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test - -on: - pull_request: - branches: [ main ] - paths: - - 'src/**' - - 'tests/**' - - 'package.json' - - 'pnpm-lock.yaml' - - 'tsconfig.json' - - 'jest.config.js' - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: latest - - - name: Install dependencies - run: pnpm install - - - name: Run tests - run: pnpm test diff --git a/README.md b/README.md index a9fc05a..9608836 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Automated queue cleaner for Sonarr that removes stuck downloads based on configu | `BLOCK_REMOVED_QUALITY_RELEASES` | `false` | Add quality-blocked items to blocklist | | `REMOVE_ARCHIVE_BLOCKED` | `false` | Remove items stuck due to archive files | | `BLOCK_REMOVED_ARCHIVE_RELEASES` | `false` | Add archive-blocked items to blocklist | +| `REMOVE_NO_FILES_RELEASES` | `false` | Remove items with no eligible files | +| `BLOCK_REMOVED_NO_FILES_RELEASES` | `false` | Add no-files items to blocklist | | `SCHEDULE` | `*/5 * * * *` | Cron schedule (every 5 minutes) | | `LOG_LEVEL` | `info` | Logging level | @@ -54,6 +56,8 @@ services: - BLOCK_REMOVED_QUALITY_RELEASES=false - REMOVE_ARCHIVE_BLOCKED=false - BLOCK_REMOVED_ARCHIVE_RELEASES=false + - REMOVE_NO_FILES_RELEASES=false + - BLOCK_REMOVED_NO_FILES_RELEASES=false - SCHEDULE=*/5 * * * * - LOG_LEVEL=info restart: unless-stopped diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..3902878 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,88 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + // Apply recommended rules + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.strict, + ...tseslint.configs.stylistic, + + // Global configuration + { + languageOptions: { + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + + // File-specific configurations + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + // TypeScript-specific rules + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-var-requires': 'error', + '@typescript-eslint/no-inferrable-types': 'error', + + // Code quality + 'no-console': 'off', // Allow console for this CLI app + 'prefer-const': 'error', + 'no-var': 'error', + 'eqeqeq': ['error', 'always'], + 'curly': ['error', 'all'], + + // Style consistency + 'indent': ['error', 4], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + 'comma-dangle': ['error', 'never'], + 'object-curly-spacing': ['error', 'always'], + 'array-bracket-spacing': ['error', 'never'], + }, + }, + + // Test files configuration + { + files: ['**/*.test.ts', '**/*.spec.ts', 'tests/**/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + 'no-console': 'off', + }, + }, + + // Configuration files + { + files: ['*.config.js', '*.config.mjs', '*.config.ts'], + languageOptions: { + globals: { + module: 'readonly', + require: 'readonly', + __dirname: 'readonly', + process: 'readonly', + }, + parserOptions: { + project: null, + }, + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + }, + }, + + // Ignore patterns + { + ignores: [ + 'dist/**', + 'node_modules/**', + 'coverage/**', + '*.d.ts', + ], + } +); diff --git a/package.json b/package.json index b01e771..68c95c7 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,36 @@ { - "name": "arr-queue-cleaner", - "version": "1.0.0", - "description": "Automated queue cleaner for Sonarr and Radarr", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx --watch src/index.ts", - "test": "jest", - "test:watch": "jest --watch" - }, - "dependencies": { - "axios": "^1.6.0", - "cron": "^3.1.0", - "dotenv": "^16.3.0" - }, - "devDependencies": { - "@types/node": "^24.3.1", - "@types/jest": "^29.5.0", - "jest": "^29.7.0", - "ts-jest": "^29.1.0", - "tsx": "^4.0.0", - "typescript": "^5.0.0" - }, - "engines": { - "node": ">=22" - } + "name": "arr-queue-cleaner", + "version": "1.0.0", + "description": "Automated queue cleaner for Sonarr and Radarr", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx --watch src/index.ts", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "dependencies": { + "axios": "^1.6.0", + "cron": "^3.1.0", + "dotenv": "^16.3.0" + }, + "devDependencies": { + "@eslint/js": "^9.35.0", + "@types/node": "^24.3.1", + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^8.43.0", + "@typescript-eslint/parser": "^8.43.0", + "eslint": "^9.35.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.43.0" + }, + "engines": { + "node": ">=22" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc0ee6e..974e616 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,12 +18,24 @@ importers: specifier: ^16.3.0 version: 16.6.1 devDependencies: + '@eslint/js': + specifier: ^9.35.0 + version: 9.35.0 '@types/jest': specifier: ^29.5.0 version: 29.5.14 '@types/node': specifier: ^24.3.1 version: 24.3.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.43.0 + version: 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.43.0 + version: 8.43.0(eslint@9.35.0)(typescript@5.9.2) + eslint: + specifier: ^9.35.0 + version: 9.35.0 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@24.3.1) @@ -36,6 +48,9 @@ importers: typescript: specifier: ^5.0.0 version: 5.9.2 + typescript-eslint: + specifier: ^8.43.0 + version: 8.43.0(eslint@9.35.0)(typescript@5.9.2) packages: @@ -364,6 +379,60 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.35.0': + resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -451,6 +520,18 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -472,6 +553,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -487,6 +571,9 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/luxon@3.4.2': resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} @@ -502,6 +589,78 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@typescript-eslint/eslint-plugin@8.43.0': + resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.43.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.43.0': + resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.43.0': + resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.43.0': + resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.43.0': + resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.43.0': + resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.43.0': + resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.43.0': + resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.43.0': + resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.43.0': + resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -525,6 +684,9 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -562,6 +724,9 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -672,6 +837,9 @@ packages: babel-plugin-macros: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -738,11 +906,57 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.35.0: + resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -755,12 +969,29 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -769,6 +1000,17 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -820,10 +1062,22 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -831,6 +1085,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -859,6 +1116,18 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -882,6 +1151,10 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -890,6 +1163,10 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1061,19 +1338,35 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -1082,6 +1375,10 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1089,9 +1386,16 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1116,6 +1420,10 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1135,6 +1443,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1168,6 +1480,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -1180,10 +1496,18 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -1218,6 +1542,10 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1229,9 +1557,16 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -1243,6 +1578,10 @@ packages: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1259,6 +1598,13 @@ packages: engines: {node: '>= 0.4'} hasBin: true + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1347,6 +1693,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-jest@29.4.1: resolution: {integrity: sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -1379,6 +1731,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -1391,6 +1747,13 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + typescript-eslint@8.43.0: + resolution: {integrity: sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -1410,6 +1773,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -1422,6 +1788,10 @@ packages: engines: {node: '>= 8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -1729,6 +2099,61 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0)': + dependencies: + eslint: 9.35.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.35.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -1915,6 +2340,18 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -1946,6 +2383,8 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@types/estree@1.0.8': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 24.3.1 @@ -1965,6 +2404,8 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/json-schema@7.0.15': {} + '@types/luxon@3.4.2': {} '@types/node@24.3.1': @@ -1979,6 +2420,112 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.43.0 + eslint: 9.35.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.1 + eslint: 9.35.0 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.43.0(typescript@5.9.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) + '@typescript-eslint/types': 8.43.0 + debug: 4.4.1 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.43.0': + dependencies: + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + + '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.9.2) + debug: 4.4.1 + eslint: 9.35.0 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.43.0': {} + + '@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.2)': + dependencies: + '@typescript-eslint/project-service': 8.43.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.2) + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.43.0(eslint@9.35.0)(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + eslint: 9.35.0 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.43.0': + dependencies: + '@typescript-eslint/types': 8.43.0 + eslint-visitor-keys: 4.2.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -2000,6 +2547,8 @@ snapshots: dependencies: sprintf-js: 1.0.3 + argparse@2.0.1: {} + asynckit@0.4.0: {} axios@1.11.0: @@ -2072,6 +2621,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -2173,6 +2726,8 @@ snapshots: dedent@1.6.0: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} delayed-stream@1.0.0: {} @@ -2247,8 +2802,77 @@ snapshots: escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.35.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.35.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -2271,12 +2895,32 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + fb-watchman@2.0.2: dependencies: bser: 2.1.1 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2286,6 +2930,18 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + follow-redirects@1.15.11: {} form-data@4.0.4: @@ -2333,6 +2989,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -2342,10 +3006,14 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + globals@14.0.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -2371,6 +3039,15 @@ snapshots: human-signals@2.1.0: {} + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -2391,10 +3068,16 @@ snapshots: dependencies: hasown: 2.0.2 + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-number@7.0.0: {} is-stream@2.0.1: {} @@ -2757,24 +3440,49 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@3.0.3: {} leven@3.1.0: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lines-and-columns@1.2.4: {} locate-path@5.0.0: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2795,6 +3503,8 @@ snapshots: merge-stream@2.0.0: {} + merge2@1.4.1: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -2812,6 +3522,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + minimist@1.2.8: {} ms@2.1.3: {} @@ -2838,6 +3552,15 @@ snapshots: dependencies: mimic-fn: 2.1.0 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -2850,8 +3573,16 @@ snapshots: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-try@2.2.0: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 @@ -2877,6 +3608,8 @@ snapshots: dependencies: find-up: 4.1.0 + prelude-ls@1.2.1: {} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -2890,8 +3623,12 @@ snapshots: proxy-from-env@1.1.0: {} + punycode@2.3.1: {} + pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} + react-is@18.3.1: {} require-directory@2.1.1: {} @@ -2900,6 +3637,8 @@ snapshots: dependencies: resolve-from: 5.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -2912,6 +3651,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + semver@6.3.1: {} semver@7.7.2: {} @@ -2984,6 +3729,10 @@ snapshots: dependencies: is-number: 7.0.0 + ts-api-utils@2.1.0(typescript@5.9.2): + dependencies: + typescript: 5.9.2 + ts-jest@29.4.1(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.3))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.3.1))(typescript@5.9.2): dependencies: bs-logger: 0.2.6 @@ -3011,12 +3760,27 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-detect@4.0.8: {} type-fest@0.21.3: {} type-fest@4.41.0: {} + typescript-eslint@8.43.0(eslint@9.35.0)(typescript@5.9.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.9.2) + eslint: 9.35.0 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + typescript@5.9.2: {} uglify-js@3.19.3: @@ -3030,6 +3794,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.30 @@ -3044,6 +3812,8 @@ snapshots: dependencies: isexe: 2.0.0 + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wrap-ansi@7.0.0: diff --git a/src/cleaner.ts b/src/cleaner.ts index ad5dc61..9c895b9 100644 --- a/src/cleaner.ts +++ b/src/cleaner.ts @@ -1,99 +1,98 @@ import { SonarrClient } from './sonarr'; -import { Config, QueueItem } from './types'; +import { Config, QueueItem, RuleMatch } from './types'; export class QueueCleaner { - private config: Config; - private sonarr: SonarrClient; + private config: Config; + private sonarr: SonarrClient; - constructor(config: Config) { - this.config = config; - this.sonarr = new SonarrClient(config.sonarr.host, config.sonarr.apiKey, config.logLevel); - } - - private log(level: string, message: string, data?: any): void { - if (level === 'debug' && this.config.logLevel !== 'debug') return; - const output = data ? `${message}: ${JSON.stringify(data)}` : message; - console.log(`[${level.toUpperCase()}] ${output}`); - } - - async cleanQueue(): Promise { - if (!this.config.sonarr.enabled) return; - - try { - const queue = await this.sonarr.getQueue(); - const itemsToProcess = queue.filter(item => this.shouldRemoveItem(item)); - - for (const item of itemsToProcess) { - await this.processItem(item); - } - - if (itemsToProcess.length > 0) { - console.log(`Processed ${itemsToProcess.length} queue items`); - } - } catch (error) { - console.error('Error cleaning queue:', (error as Error).message); - } - } - - shouldRemoveItem(item: QueueItem): boolean { - if (item.status !== 'completed') { - this.log('debug', 'Item not completed yet', item.title); - return false; - } - if (item.trackedDownloadStatus !== 'warning') { - this.log('debug', 'Item not in download warning status', item.title); - return false; - } - if (item.trackedDownloadState !== 'importPending') { - this.log('debug', 'Item not stuck in importing', item.title); - return false; - } - if (!item.statusMessages?.length) { - this.log('info', 'Item has no status messages', item.title); - return false; + constructor(config: Config) { + this.config = config; + this.sonarr = new SonarrClient(config.sonarr.host, config.sonarr.apiKey, config.logLevel); } - this.log('debug', 'Got item to check', { - title: item.title, - status: item.status, - trackedDownloadStatus: item.trackedDownloadStatus, - trackedDownloadState: item.trackedDownloadState, - statusMessages: item.statusMessages - }); - - return item.statusMessages.some(msg => { - const hasQualityIssue = this.config.rules.removeQualityBlocked && - msg.messages?.some(m => m.includes('upgrade for existing episode')); - - const hasArchiveIssue = this.config.rules.removeArchiveBlocked && - msg.messages?.some(m => m.includes('archive file')); - - if (hasQualityIssue) this.log('debug', 'Item has quality issue', item.title); - if (hasArchiveIssue) this.log('debug', 'Item has archive issue', item.title); - - return hasQualityIssue || hasArchiveIssue; - }); - } - - async processItem(item: QueueItem): Promise { - try { - const isArchiveIssue = item.statusMessages?.some(msg => - msg.messages?.some(m => m.includes('archive file')) - ); - - const shouldBlock = isArchiveIssue ? - this.config.rules.blockRemovedArchiveReleases : - this.config.rules.blockRemovedQualityReleases; - - if (shouldBlock) { - await this.sonarr.blockRelease(item.id); - console.log(`Blocked and removed: ${item.title}`); - } else { - await this.sonarr.removeFromQueue(item.id); - console.log(`Removed: ${item.title}`); - } - } catch (error) { - console.error(`Error processing ${item.title}:`, (error as Error).message); + private log(level: string, message: string, data?: unknown): void { + if (level === 'debug' && this.config.logLevel !== 'debug') {return;} + const output = data ? `${message}: ${JSON.stringify(data)}` : message; + console.log(`[${level.toUpperCase()}] ${output}`); + } + + async cleanQueue(): Promise { + if (!this.config.sonarr.enabled) {return;} + + try { + const queue = await this.sonarr.getQueue(); + const itemsToProcess: { item: QueueItem; rule: RuleMatch }[] = []; + + for (const item of queue) { + const rule = this.evaluateRules(item); + if (rule) { + itemsToProcess.push({ item, rule }); + } + } + + for (const { item, rule } of itemsToProcess) { + await this.processItem(item, rule); + } + + if (itemsToProcess.length > 0) { + console.log(`Processed ${itemsToProcess.length} queue items`); + } + } catch (error) { + console.error('Error cleaning queue:', (error as Error).message); + } + } + + private evaluateRules(item: QueueItem): RuleMatch | null { + if (item.status !== 'completed' || + item.trackedDownloadStatus !== 'warning' || + item.trackedDownloadState !== 'importPending' || + !item.statusMessages?.length) { + return null; + } + + this.log('debug', 'Evaluating rules for item', { + title: item.title, + status: item.status, + trackedDownloadStatus: item.trackedDownloadStatus, + trackedDownloadState: item.trackedDownloadState, + statusMessages: item.statusMessages + }); + + for (const msg of item.statusMessages) { + if (!msg.messages?.length) {continue;} + + for (const message of msg.messages) { + if (this.config.rules.removeQualityBlocked && message.includes('upgrade for existing episode')) { + this.log('debug', 'Item matched quality rule', item.title); + return { type: 'quality', shouldBlock: this.config.rules.blockRemovedQualityReleases }; + } + + if (this.config.rules.removeArchiveBlocked && message.includes('archive file')) { + this.log('debug', 'Item matched archive rule', item.title); + return { type: 'archive', shouldBlock: this.config.rules.blockRemovedArchiveReleases }; + } + + if (this.config.rules.removeNoFilesReleases && message.includes('No files found are eligible')) { + this.log('debug', 'Item matched no files rule', item.title); + return { type: 'noFiles', shouldBlock: this.config.rules.blockRemovedNoFilesReleases }; + } + } + } + + return null; + } + + private async processItem(item: QueueItem, rule: RuleMatch): Promise { + try { + if (rule.shouldBlock) { + await this.sonarr.blockRelease(item.id); + console.log(`Blocked and removed (${rule.type}): ${item.title}`); + } else { + await this.sonarr.removeFromQueue(item.id); + console.log(`Removed (${rule.type}): ${item.title}`); + } + } catch (error) { + console.error(`Error processing ${item.title}:`, (error as Error).message); + } } - } } diff --git a/src/config.ts b/src/config.ts index a60efab..caa4341 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,24 +4,26 @@ import { Config } from './types'; dotenvConfig(); const config: Config = { - sonarr: { - host: process.env.SONARR_HOST || 'http://localhost:8989', - apiKey: process.env.SONARR_API_KEY || '', - enabled: !!(process.env.SONARR_HOST && process.env.SONARR_HOST.trim() !== '') - }, - rules: { - removeQualityBlocked: process.env.REMOVE_QUALITY_BLOCKED === 'true', - blockRemovedQualityReleases: process.env.BLOCK_REMOVED_QUALITY_RELEASES === 'true', - removeArchiveBlocked: process.env.REMOVE_ARCHIVE_BLOCKED === 'true', - blockRemovedArchiveReleases: process.env.BLOCK_REMOVED_ARCHIVE_RELEASES === 'true' - }, - schedule: process.env.SCHEDULE || '*/5 * * * *', - logLevel: process.env.LOG_LEVEL || 'info' + sonarr: { + host: process.env.SONARR_HOST || 'http://localhost:8989', + apiKey: process.env.SONARR_API_KEY || '', + enabled: !!(process.env.SONARR_HOST && process.env.SONARR_HOST.trim() !== '') + }, + rules: { + removeQualityBlocked: process.env.REMOVE_QUALITY_BLOCKED === 'true', + blockRemovedQualityReleases: process.env.BLOCK_REMOVED_QUALITY_RELEASES === 'true', + removeArchiveBlocked: process.env.REMOVE_ARCHIVE_BLOCKED === 'true', + blockRemovedArchiveReleases: process.env.BLOCK_REMOVED_ARCHIVE_RELEASES === 'true', + removeNoFilesReleases: process.env.REMOVE_NO_FILES_RELEASES === 'true', + blockRemovedNoFilesReleases: process.env.BLOCK_REMOVED_NO_FILES_RELEASES === 'true' + }, + schedule: process.env.SCHEDULE || '*/5 * * * *', + logLevel: process.env.LOG_LEVEL || 'info' }; if (!config.sonarr.apiKey) { - console.error('SONARR_API_KEY is required'); - process.exit(1); + console.error('SONARR_API_KEY is required'); + process.exit(1); } export default config; diff --git a/src/sonarr.ts b/src/sonarr.ts index 1ff3f94..8fa3e9d 100644 --- a/src/sonarr.ts +++ b/src/sonarr.ts @@ -2,44 +2,44 @@ import axios, { AxiosInstance } from 'axios'; import { QueueItem } from './types'; export class SonarrClient { - private client: AxiosInstance; - private host: string; - private logLevel: string; + private client: AxiosInstance; + private host: string; + private logLevel: string; - constructor(host: string, apiKey: string, logLevel: string = 'info') { - this.host = host; - this.logLevel = logLevel; - this.client = axios.create({ - baseURL: `${host}/api/v3`, - headers: { 'X-Api-Key': apiKey } - }); - } + constructor(host: string, apiKey: string, logLevel = 'info') { + this.host = host; + this.logLevel = logLevel; + this.client = axios.create({ + baseURL: `${host}/api/v3`, + headers: { 'X-Api-Key': apiKey } + }); + } - private log(level: string, message: string): void { - if (level === 'debug' && this.logLevel !== 'debug') return; - console.log(`[${level.toUpperCase()}] ${message}`); - } + private log(level: string, message: string): void { + if (level === 'debug' && this.logLevel !== 'debug') {return;} + console.log(`[${level.toUpperCase()}] ${message}`); + } - async getQueue(): Promise { - const { data } = await this.client.get('/queue'); - this.log('debug', `Successfully contacted Sonarr API at ${this.host}/api/v3/queue`); - this.log('debug', `Queue response: ${JSON.stringify(data, null, 2)}`); - return data.records || data; - } + async getQueue(): Promise { + const { data } = await this.client.get('/queue'); + this.log('debug', `Successfully contacted Sonarr API at ${this.host}/api/v3/queue`); + this.log('debug', `Queue response: ${JSON.stringify(data, null, 2)}`); + return data.records || data; + } - async removeFromQueue(id: number): Promise { - const response = await this.client.delete(`/queue/${id}`, { - params: { removeFromClient: true, blocklist: false } - }); - this.log('debug', `Successfully removed queue item ${id} from Sonarr`); - this.log('debug', `Remove response: ${JSON.stringify(response.data, null, 2)}`); - } + async removeFromQueue(id: number): Promise { + const response = await this.client.delete(`/queue/${id}`, { + params: { removeFromClient: true, blocklist: false } + }); + this.log('debug', `Successfully removed queue item ${id} from Sonarr`); + this.log('debug', `Remove response: ${JSON.stringify(response.data, null, 2)}`); + } - async blockRelease(id: number): Promise { - const response = await this.client.delete(`/queue/${id}`, { - params: { removeFromClient: true, blocklist: true } - }); - this.log('debug', `Successfully blocked and removed queue item ${id} from Sonarr`); - this.log('debug', `Block response: ${JSON.stringify(response.data, null, 2)}`); - } + async blockRelease(id: number): Promise { + const response = await this.client.delete(`/queue/${id}`, { + params: { removeFromClient: true, blocklist: true } + }); + this.log('debug', `Successfully blocked and removed queue item ${id} from Sonarr`); + this.log('debug', `Block response: ${JSON.stringify(response.data, null, 2)}`); + } } diff --git a/src/types.ts b/src/types.ts index 5177075..f71c524 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,29 +1,38 @@ export interface Config { - sonarr: { - host: string; - apiKey: string; - enabled: boolean; - }; - rules: { - removeQualityBlocked: boolean; - blockRemovedQualityReleases: boolean; - removeArchiveBlocked: boolean; - blockRemovedArchiveReleases: boolean; - }; - schedule: string; - logLevel: string; + sonarr: { + host: string; + apiKey: string; + enabled: boolean; + }; + rules: { + removeQualityBlocked: boolean; + blockRemovedQualityReleases: boolean; + removeArchiveBlocked: boolean; + blockRemovedArchiveReleases: boolean; + removeNoFilesReleases: boolean; + blockRemovedNoFilesReleases: boolean; + }; + schedule: string; + logLevel: string; } export interface QueueItem { - id: number; - title: string; - status: string; - trackedDownloadStatus: string; - trackedDownloadState: string; - statusMessages: StatusMessage[]; + id: number; + title: string; + status: string; + trackedDownloadStatus: string; + trackedDownloadState: string; + statusMessages: StatusMessage[]; } export interface StatusMessage { - title?: string; - messages?: string[]; + title?: string; + messages?: string[]; +} + +export type RuleType = 'quality' | 'archive' | 'noFiles'; + +export interface RuleMatch { + type: RuleType; + shouldBlock: boolean; } diff --git a/tests/cleaner.test.ts b/tests/cleaner.test.ts index 9a0a212..61e081d 100644 --- a/tests/cleaner.test.ts +++ b/tests/cleaner.test.ts @@ -1,249 +1,306 @@ import { QueueCleaner } from '../src/cleaner'; import { SonarrClient } from '../src/sonarr'; -import { createMockConfig, createMockQueueItem, createQualityBlockedItem, createArchiveBlockedItem } from './test-utils'; +import { createMockConfig, createMockQueueItem, createQualityBlockedItem, createArchiveBlockedItem, createNoFilesBlockedItem } from './test-utils'; jest.mock('../src/sonarr'); const MockedSonarrClient = SonarrClient as jest.MockedClass; describe('QueueCleaner', () => { - let mockSonarrClient: jest.Mocked; + let mockSonarrClient: jest.Mocked; - beforeEach(() => { - mockSonarrClient = { - getQueue: jest.fn(), - removeFromQueue: jest.fn(), - blockRelease: jest.fn() - } as any; - MockedSonarrClient.mockImplementation(() => mockSonarrClient); - - jest.spyOn(console, 'log').mockImplementation(); - jest.spyOn(console, 'error').mockImplementation(); - }); + beforeEach(() => { + mockSonarrClient = { + getQueue: jest.fn(), + removeFromQueue: jest.fn(), + blockRelease: jest.fn() + } as any; + MockedSonarrClient.mockImplementation(() => mockSonarrClient); - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('shouldRemoveItem', () => { - it('should return false for non-completed items', () => { - const config = createMockConfig({ rules: { removeQualityBlocked: true } }); - const cleaner = new QueueCleaner(config); - const item = createMockQueueItem({ status: 'downloading' }); - - expect(cleaner.shouldRemoveItem(item)).toBe(false); + jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); }); - it('should return false for items without warning status', () => { - const config = createMockConfig({ rules: { removeQualityBlocked: true } }); - const cleaner = new QueueCleaner(config); - const item = createMockQueueItem({ trackedDownloadStatus: 'ok' }); - - expect(cleaner.shouldRemoveItem(item)).toBe(false); + afterEach(() => { + jest.restoreAllMocks(); }); - it('should return false for items not in importPending state', () => { - const config = createMockConfig({ rules: { removeQualityBlocked: true } }); - const cleaner = new QueueCleaner(config); - const item = createMockQueueItem({ trackedDownloadState: 'downloading' }); + describe('cleanQueue', () => { + it('should not process when sonarr is disabled', async () => { + const config = createMockConfig({ sonarr: { host: '', enabled: false } }); + const cleaner = new QueueCleaner(config); - expect(cleaner.shouldRemoveItem(item)).toBe(false); - }); + await cleaner.cleanQueue(); - it('should return false for items without status messages', () => { - const config = createMockConfig({ rules: { removeQualityBlocked: true } }); - const cleaner = new QueueCleaner(config); - const item = createMockQueueItem({ statusMessages: [] }); - - expect(cleaner.shouldRemoveItem(item)).toBe(false); - }); - - describe('quality blocked items', () => { - it('should return true when removeQualityBlocked is enabled and item has quality issue', () => { - const config = createMockConfig({ rules: { removeQualityBlocked: true } }); - const cleaner = new QueueCleaner(config); - const item = createQualityBlockedItem(); - - expect(cleaner.shouldRemoveItem(item)).toBe(true); - }); - - it('should return false when removeQualityBlocked is disabled', () => { - const config = createMockConfig({ rules: { removeQualityBlocked: false } }); - const cleaner = new QueueCleaner(config); - const item = createQualityBlockedItem(); - - expect(cleaner.shouldRemoveItem(item)).toBe(false); - }); - }); - - describe('archive blocked items', () => { - it('should return true when removeArchiveBlocked is enabled and item has archive issue', () => { - const config = createMockConfig({ rules: { removeArchiveBlocked: true } }); - const cleaner = new QueueCleaner(config); - const item = createArchiveBlockedItem(); - - expect(cleaner.shouldRemoveItem(item)).toBe(true); - }); - - it('should return false when removeArchiveBlocked is disabled', () => { - const config = createMockConfig({ rules: { removeArchiveBlocked: false } }); - const cleaner = new QueueCleaner(config); - const item = createArchiveBlockedItem(); - - expect(cleaner.shouldRemoveItem(item)).toBe(false); - }); - }); - }); - - describe('processItem', () => { - describe('quality blocked items', () => { - it('should remove without blocking when blockRemovedQualityReleases is false', async () => { - const config = createMockConfig({ - rules: { - removeQualityBlocked: true, - blockRemovedQualityReleases: false - } + expect(mockSonarrClient.getQueue).not.toHaveBeenCalled(); }); - const cleaner = new QueueCleaner(config); - const item = createQualityBlockedItem(); - await cleaner.processItem(item); + it('should skip non-completed items', async () => { + const config = createMockConfig({ rules: { removeQualityBlocked: true } }); + const cleaner = new QueueCleaner(config); + const items = [createMockQueueItem({ status: 'downloading' })]; - expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledWith(123); - expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); - }); + mockSonarrClient.getQueue.mockResolvedValue(items); - it('should block when blockRemovedQualityReleases is true', async () => { - const config = createMockConfig({ - rules: { - removeQualityBlocked: true, - blockRemovedQualityReleases: true - } + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); }); - const cleaner = new QueueCleaner(config); - const item = createQualityBlockedItem(); - await cleaner.processItem(item); + it('should skip items without warning status', async () => { + const config = createMockConfig({ rules: { removeQualityBlocked: true } }); + const cleaner = new QueueCleaner(config); + const items = [createMockQueueItem({ trackedDownloadStatus: 'ok' })]; - expect(mockSonarrClient.blockRelease).toHaveBeenCalledWith(123); - expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); - }); - }); + mockSonarrClient.getQueue.mockResolvedValue(items); - describe('archive blocked items', () => { - it('should remove without blocking when blockRemovedArchiveReleases is false', async () => { - const config = createMockConfig({ - rules: { - removeArchiveBlocked: true, - blockRemovedArchiveReleases: false - } + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); }); - const cleaner = new QueueCleaner(config); - const item = createArchiveBlockedItem(); - await cleaner.processItem(item); + it('should skip items not in importPending state', async () => { + const config = createMockConfig({ rules: { removeQualityBlocked: true } }); + const cleaner = new QueueCleaner(config); + const items = [createMockQueueItem({ trackedDownloadState: 'downloading' })]; - expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledWith(123); - expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); - }); + mockSonarrClient.getQueue.mockResolvedValue(items); - it('should block when blockRemovedArchiveReleases is true', async () => { - const config = createMockConfig({ - rules: { - removeArchiveBlocked: true, - blockRemovedArchiveReleases: true - } + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); }); - const cleaner = new QueueCleaner(config); - const item = createArchiveBlockedItem(); - await cleaner.processItem(item); + it('should skip items without status messages', async () => { + const config = createMockConfig({ rules: { removeQualityBlocked: true } }); + const cleaner = new QueueCleaner(config); + const items = [createMockQueueItem({ statusMessages: [] })]; - expect(mockSonarrClient.blockRelease).toHaveBeenCalledWith(123); - expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); - }); - }); - }); + mockSonarrClient.getQueue.mockResolvedValue(items); - describe('cleanQueue', () => { - it('should not process when sonarr host is empty', async () => { - const config = createMockConfig({ sonarr: { host: '', enabled: false } }); - const cleaner = new QueueCleaner(config); + await cleaner.cleanQueue(); - await cleaner.cleanQueue(); + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); - expect(mockSonarrClient.getQueue).not.toHaveBeenCalled(); + describe('quality blocked items', () => { + it('should remove quality blocked items when enabled', async () => { + const config = createMockConfig({ rules: { removeQualityBlocked: true } }); + const cleaner = new QueueCleaner(config); + const items = [createQualityBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledWith(123); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + + it('should block quality blocked items when blocking enabled', async () => { + const config = createMockConfig({ + rules: { + removeQualityBlocked: true, + blockRemovedQualityReleases: true + } + }); + const cleaner = new QueueCleaner(config); + const items = [createQualityBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.blockRelease).toHaveBeenCalledWith(123); + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + }); + + it('should skip quality blocked items when disabled', async () => { + const config = createMockConfig({ rules: { removeQualityBlocked: false } }); + const cleaner = new QueueCleaner(config); + const items = [createQualityBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + }); + + describe('archive blocked items', () => { + it('should remove archive blocked items when enabled', async () => { + const config = createMockConfig({ rules: { removeArchiveBlocked: true } }); + const cleaner = new QueueCleaner(config); + const items = [createArchiveBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledWith(123); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + + it('should block archive blocked items when blocking enabled', async () => { + const config = createMockConfig({ + rules: { + removeArchiveBlocked: true, + blockRemovedArchiveReleases: true + } + }); + const cleaner = new QueueCleaner(config); + const items = [createArchiveBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.blockRelease).toHaveBeenCalledWith(123); + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + }); + + it('should skip archive blocked items when disabled', async () => { + const config = createMockConfig({ rules: { removeArchiveBlocked: false } }); + const cleaner = new QueueCleaner(config); + const items = [createArchiveBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + }); + + describe('no files blocked items', () => { + it('should remove no files blocked items when enabled', async () => { + const config = createMockConfig({ rules: { removeNoFilesReleases: true } }); + const cleaner = new QueueCleaner(config); + const items = [createNoFilesBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledWith(123); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + + it('should block no files blocked items when blocking enabled', async () => { + const config = createMockConfig({ + rules: { + removeNoFilesReleases: true, + blockRemovedNoFilesReleases: true + } + }); + const cleaner = new QueueCleaner(config); + const items = [createNoFilesBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.blockRelease).toHaveBeenCalledWith(123); + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + }); + + it('should skip no files blocked items when disabled', async () => { + const config = createMockConfig({ rules: { removeNoFilesReleases: false } }); + const cleaner = new QueueCleaner(config); + const items = [createNoFilesBlockedItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + }); + + it('should process multiple matching items', async () => { + const config = createMockConfig({ + rules: { + removeQualityBlocked: true, + removeArchiveBlocked: true, + removeNoFilesReleases: true + } + }); + const cleaner = new QueueCleaner(config); + + const items = [ + createQualityBlockedItem(), + createArchiveBlockedItem(), + createNoFilesBlockedItem(), + createMockQueueItem({ status: 'downloading' }) // Should be ignored + ]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledTimes(3); + }); + + it('should handle errors gracefully', async () => { + const config = createMockConfig({ rules: { removeQualityBlocked: true } }); + const cleaner = new QueueCleaner(config); + + mockSonarrClient.getQueue.mockRejectedValue(new Error('API Error')); + + await cleaner.cleanQueue(); + + expect(console.error).toHaveBeenCalledWith('Error cleaning queue:', 'API Error'); + }); }); - it('should process all matching items', async () => { - const config = createMockConfig({ - rules: { - removeQualityBlocked: true, - removeArchiveBlocked: true - } - }); - const cleaner = new QueueCleaner(config); - - const items = [ - createQualityBlockedItem(), - createArchiveBlockedItem(), - createMockQueueItem({ status: 'downloading' }) // Should be ignored - ]; - - mockSonarrClient.getQueue.mockResolvedValue(items); + describe('configuration combinations', () => { + const testCases = [ + { + name: 'all rules disabled', + config: { removeQualityBlocked: false, removeArchiveBlocked: false, removeNoFilesReleases: false }, + expectProcessed: 0 + }, + { + name: 'only quality removal enabled', + config: { removeQualityBlocked: true, removeArchiveBlocked: false, removeNoFilesReleases: false }, + expectProcessed: 1 + }, + { + name: 'only archive removal enabled', + config: { removeQualityBlocked: false, removeArchiveBlocked: true, removeNoFilesReleases: false }, + expectProcessed: 1 + }, + { + name: 'only no files removal enabled', + config: { removeQualityBlocked: false, removeArchiveBlocked: false, removeNoFilesReleases: true }, + expectProcessed: 1 + }, + { + name: 'all removals enabled', + config: { removeQualityBlocked: true, removeArchiveBlocked: true, removeNoFilesReleases: true }, + expectProcessed: 3 + } + ]; - await cleaner.cleanQueue(); + testCases.forEach(({ name, config: rules, expectProcessed }) => { + it(`should handle ${name}`, async () => { + const config = createMockConfig({ rules }); + const cleaner = new QueueCleaner(config); - expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledTimes(2); + const items = [createQualityBlockedItem(), createArchiveBlockedItem(), createNoFilesBlockedItem()]; + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledTimes(expectProcessed); + }); + }); }); - - it('should handle errors gracefully', async () => { - const config = createMockConfig({ rules: { removeQualityBlocked: true } }); - const cleaner = new QueueCleaner(config); - - mockSonarrClient.getQueue.mockRejectedValue(new Error('API Error')); - - await cleaner.cleanQueue(); - - expect(console.error).toHaveBeenCalledWith('Error cleaning queue:', 'API Error'); - }); - }); - - describe('configuration combinations', () => { - const testCases = [ - { - name: 'all rules disabled', - config: { removeQualityBlocked: false, removeArchiveBlocked: false }, - expectProcessed: 0 - }, - { - name: 'only quality removal enabled', - config: { removeQualityBlocked: true, removeArchiveBlocked: false }, - expectProcessed: 1 - }, - { - name: 'only archive removal enabled', - config: { removeQualityBlocked: false, removeArchiveBlocked: true }, - expectProcessed: 1 - }, - { - name: 'both removals enabled', - config: { removeQualityBlocked: true, removeArchiveBlocked: true }, - expectProcessed: 2 - } - ]; - - testCases.forEach(({ name, config: rules, expectProcessed }) => { - it(`should handle ${name}`, async () => { - const config = createMockConfig({ rules }); - const cleaner = new QueueCleaner(config); - - const items = [createQualityBlockedItem(), createArchiveBlockedItem()]; - mockSonarrClient.getQueue.mockResolvedValue(items); - - await cleaner.cleanQueue(); - - expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledTimes(expectProcessed); - }); - }); - }); }); diff --git a/tests/sonarr.test.ts b/tests/sonarr.test.ts index 33f147e..177cb7a 100644 --- a/tests/sonarr.test.ts +++ b/tests/sonarr.test.ts @@ -5,68 +5,68 @@ jest.mock('axios'); const mockedAxios = axios as jest.Mocked; describe('SonarrClient', () => { - let client: SonarrClient; - let mockAxiosInstance: any; + let client: SonarrClient; + let mockAxiosInstance: any; - beforeEach(() => { - mockAxiosInstance = { - get: jest.fn(), - delete: jest.fn() - }; - mockedAxios.create.mockReturnValue(mockAxiosInstance); - - // Mock console.log to avoid test output - jest.spyOn(console, 'log').mockImplementation(); - - client = new SonarrClient('http://localhost:8989', 'test-key', 'info'); - }); + beforeEach(() => { + mockAxiosInstance = { + get: jest.fn(), + delete: jest.fn() + }; + mockedAxios.create.mockReturnValue(mockAxiosInstance); - afterEach(() => { - jest.restoreAllMocks(); - }); + // Mock console.log to avoid test output + jest.spyOn(console, 'log').mockImplementation(); - describe('getQueue', () => { - it('should return queue records', async () => { - const mockData = { records: [{ id: 1, title: 'test' }] }; - mockAxiosInstance.get.mockResolvedValue({ data: mockData }); - - const result = await client.getQueue(); - - expect(mockAxiosInstance.get).toHaveBeenCalledWith('/queue'); - expect(result).toEqual(mockData.records); + client = new SonarrClient('http://localhost:8989', 'test-key', 'info'); }); - it('should return data directly if no records property', async () => { - const mockData = [{ id: 1, title: 'test' }]; - mockAxiosInstance.get.mockResolvedValue({ data: mockData }); - - const result = await client.getQueue(); - - expect(result).toEqual(mockData); + afterEach(() => { + jest.restoreAllMocks(); }); - }); - describe('removeFromQueue', () => { - it('should call delete with correct parameters', async () => { - mockAxiosInstance.delete.mockResolvedValue({ data: {} }); + describe('getQueue', () => { + it('should return queue records', async () => { + const mockData = { records: [{ id: 1, title: 'test' }] }; + mockAxiosInstance.get.mockResolvedValue({ data: mockData }); - await client.removeFromQueue(123); + const result = await client.getQueue(); - expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/queue/123', { - params: { removeFromClient: true, blocklist: false } - }); + expect(mockAxiosInstance.get).toHaveBeenCalledWith('/queue'); + expect(result).toEqual(mockData.records); + }); + + it('should return data directly if no records property', async () => { + const mockData = [{ id: 1, title: 'test' }]; + mockAxiosInstance.get.mockResolvedValue({ data: mockData }); + + const result = await client.getQueue(); + + expect(result).toEqual(mockData); + }); }); - }); - describe('blockRelease', () => { - it('should call delete with blocklist true', async () => { - mockAxiosInstance.delete.mockResolvedValue({ data: {} }); + describe('removeFromQueue', () => { + it('should call delete with correct parameters', async () => { + mockAxiosInstance.delete.mockResolvedValue({ data: {} }); - await client.blockRelease(123); + await client.removeFromQueue(123); - expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/queue/123', { - params: { removeFromClient: true, blocklist: true } - }); + expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/queue/123', { + params: { removeFromClient: true, blocklist: false } + }); + }); + }); + + describe('blockRelease', () => { + it('should call delete with blocklist true', async () => { + mockAxiosInstance.delete.mockResolvedValue({ data: {} }); + + await client.blockRelease(123); + + expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/queue/123', { + params: { removeFromClient: true, blocklist: true } + }); + }); }); - }); }); diff --git a/tests/test-utils.ts b/tests/test-utils.ts index b8a6bd6..660c7cb 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -1,47 +1,56 @@ import { Config, QueueItem } from '../src/types'; type DeepPartial = { - [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; }; export const createMockConfig = (overrides: DeepPartial = {}): Config => ({ - sonarr: { - host: 'http://localhost:8989', - apiKey: 'test-api-key', - enabled: true, - ...overrides.sonarr - }, - rules: { - removeQualityBlocked: false, - blockRemovedQualityReleases: false, - removeArchiveBlocked: false, - blockRemovedArchiveReleases: false, - ...overrides.rules - }, - schedule: overrides.schedule || '*/5 * * * *', - logLevel: overrides.logLevel || 'info' + sonarr: { + host: 'http://localhost:8989', + apiKey: 'test-api-key', + enabled: true, + ...overrides.sonarr + }, + rules: { + removeQualityBlocked: false, + blockRemovedQualityReleases: false, + removeArchiveBlocked: false, + blockRemovedArchiveReleases: false, + removeNoFilesReleases: false, + blockRemovedNoFilesReleases: false, + ...overrides.rules + }, + schedule: overrides.schedule || '*/5 * * * *', + logLevel: overrides.logLevel || 'info' }); export const createMockQueueItem = (overrides: Partial = {}): QueueItem => ({ - id: 123, - title: 'Test.Show.S01E01', - status: 'completed', - trackedDownloadStatus: 'warning', - trackedDownloadState: 'importPending', - statusMessages: [], - ...overrides + id: 123, + title: 'Test.Show.S01E01', + status: 'completed', + trackedDownloadStatus: 'warning', + trackedDownloadState: 'importPending', + statusMessages: [], + ...overrides }); -export const createQualityBlockedItem = (): QueueItem => - createMockQueueItem({ - statusMessages: [{ - messages: ['upgrade for existing episode'] - }] - }); +export const createQualityBlockedItem = (): QueueItem => + createMockQueueItem({ + statusMessages: [{ + messages: ['upgrade for existing episode'] + }] + }); -export const createArchiveBlockedItem = (): QueueItem => - createMockQueueItem({ - statusMessages: [{ - messages: ['Found archive file, might need to be extracted'] - }] - }); +export const createArchiveBlockedItem = (): QueueItem => + createMockQueueItem({ + statusMessages: [{ + messages: ['Found archive file, might need to be extracted'] + }] + }); + +export const createNoFilesBlockedItem = (): QueueItem => + createMockQueueItem({ + statusMessages: [{ + messages: ['No files found are eligible for import'] + }] + }); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..6bc05c0 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*", + "tests/**/*", + "*.config.js", + "*.config.mjs", + "*.config.ts" + ], + "exclude": ["node_modules", "dist"] +}