commit 5dca94a3fc6e4ffd029127b330d6851f7c4b3447 Author: OctoWoW Date: Fri Aug 14 01:45:50 2026 +0000 OctoLauncher 1.3.1 Manifest-based CDN updater and mod manager for the OctoWoW 1.12.1 client: launcher-owned realmlist, torrent-backed content sync with bundled aria2c, antivirus and Defender exclusion handling, hardware-aware render distance, optional client tweaks and mods, and the in-launcher news feed. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ea9f6ee --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +MAIN_VITE_SERVER_URL=http://localhost:5000 +MAIN_VITE_CLIENT_VERSION=latest diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..93d2765 --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +MAIN_VITE_SERVER_URL=https://octowow.st +MAIN_VITE_CLIENT_VERSION=latest diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..cdc1d78 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Force LF on shell scripts so git-bash can execute them as hooks on Windows +hooks/* text eol=lf +*.sh text eol=lf +*.py text eol=lf + +.gitea/** export-ignore +.env.ptr export-ignore +electron-builder.ptr.yml export-ignore +server/Dockerfile export-ignore +scripts/publish-oss.sh export-ignore diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d716655 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: Build check + +on: + push: + branches: [main, master] + pull_request: + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install JS dependencies (skip native compile) + run: npm install --ignore-scripts --no-audit --no-fund + + - name: Download Electron binary + run: node node_modules/electron/install.js + + - name: Rebuild native modules for Electron ABI + run: node_modules/.bin/electron-builder.cmd install-app-deps + + - name: Build bundles + run: npm run build + env: + ELECTRON_RUN_AS_NODE: '' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e0e4f5f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install VS 2022 Build Tools (node-gyp requirement) + run: | + choco install visualstudio2022buildtools ` + --package-parameters "--add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.Windows11SDK.22621 --includeRecommended --passive" ` + --no-progress -y + shell: powershell + + - name: Install JS dependencies (skip native compile) + run: npm install --ignore-scripts --no-audit --no-fund + + - name: Download Electron binary + run: node node_modules/electron/install.js + + - name: Rebuild native modules for Electron ABI + run: node_modules/.bin/electron-builder.cmd install-app-deps + + - name: Build and package + run: npm run dist + env: + ELECTRON_RUN_AS_NODE: '' + + - name: Upload portable exe + uses: actions/upload-artifact@v4 + with: + name: OctoLauncher-portable + path: dist/OctoLauncher.exe + + - name: Upload installer + uses: actions/upload-artifact@v4 + with: + name: OctoLauncher-installer + path: dist/OctoLauncher_Installer.exe + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/OctoLauncher.exe + dist/OctoLauncher_Installer.exe + dist/latest.yml + dist/OctoLauncher_Installer.exe.blockmap diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02e3b7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules/ + +dist*/ +out/ +release/ + +*.tsbuildinfo + +Tools/launcher/node/ + +.env +.env.local + +*.log + +.DS_Store +Thumbs.db + +scripts/ +hooks/ diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..bde2f4a --- /dev/null +++ b/BUILD.md @@ -0,0 +1,134 @@ +# OctoLauncher Build Guide (Windows) + +End-to-end setup that gets `npm run dev` and `npm run dist` working on a fresh Windows machine. Captured from a working build on Windows 10 / April 2026. + +## Changes made to the dev environment + +The project as checked out does **not** build on a default up-to-date Windows dev machine. These are the deltas applied to get it working, in order: + +1. **Added a Node version manager (`fnm`) and installed Node 20** alongside the existing Node 24. Node 24 was the system default and caused `nan` / `dll-inject` compile failures. Node 20 is now the fnm default but Node 24 is still available via `fnm use system`. +2. **Installed Visual Studio 2022 Build Tools** with the `VCTools` workload and Windows 11 SDK. Machine already had VS2026 (v18), but `node-gyp` v10 (shipped with Node 20's npm) doesn't detect it. VS2022 now lives side-by-side under `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools`. +3. **Unset `ELECTRON_RUN_AS_NODE`** per-shell before launching Electron. This var is set globally by VSCode's integrated terminal (inherited from the extension host): it is not something we can remove permanently without breaking VSCode. It has to be unset in each shell that runs `npm run dev` / `dist`. +4. **Populated `node_modules`** in both `main/` and `main/server/`. The tree was checked in empty. + +Nothing in the repo itself was modified; all fixes were environmental. If another developer checks this repo out, they need to apply items 1-4 on their own machine. The sections below are that recipe. + +--- + +## Prerequisites + +### 1. Node.js 20 (not 22, not 24) + +Node 24 breaks the `dll-inject` native module; its `nan` C++ bindings don't compile against V8 in Node 22+. Stick to Node 20 LTS. + +Install via `fnm` so you can keep your system Node separate: + +```bash +winget install Schniz.fnm --accept-source-agreements --accept-package-agreements +fnm install 20 +fnm default 20 +``` + +Verify: `node -v` should print `v20.x.x`. + +### 2. Visual Studio 2022 Build Tools (C++ workload) + +`dll-inject` and `stormlib-node` compile native addons via `node-gyp`. `node-gyp` v10 (bundled with Node 20's npm) only recognizes VS2017-2022; newer VS versions (2026 / v18) are not detected. + +```bash +winget install Microsoft.VisualStudio.2022.BuildTools \ + --accept-source-agreements --accept-package-agreements \ + --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.Windows11SDK.22621 --includeRecommended" +``` + +~6 GB download, requires admin. Even if you already have VS2026, you need VS2022 side-by-side for node-gyp. + +### 3. Python 3 (usually already present) + +`node-gyp` needs Python on PATH. Any 3.x works. + +## Install dependencies + +From the repo root: + +```bash +npm install --ignore-scripts +node node_modules/electron/install.js +node_modules/.bin/electron-builder.cmd install-app-deps +``` + +Why the three-step approach: `dll-inject` requires ClangCL if compiled against the system Node, but compiles fine against Electron's bundled V8 headers (which `electron-builder install-app-deps` uses). Running plain `npm install` fails if your VS2022 installation doesn't include the LLVM/ClangCL component; `--ignore-scripts` skips that step and lets `install-app-deps` handle it correctly. + +Or, use the provided build script which downloads a portable Node 20 and handles everything automatically: + +```powershell +.\Tools\launcher\install.ps1 +``` + +Then the server (only needed if running a local CDN, see `server/.env.example`): + +```bash +cd server +npm install +cd .. +``` + +## Critical env var: `ELECTRON_RUN_AS_NODE` + +**VSCode's integrated terminal sets `ELECTRON_RUN_AS_NODE=1`** (inherited from VSCode's extension host). This makes Electron binaries launch as plain Node, so `require('electron')` returns a path string instead of the API: the app crashes with `TypeError: Cannot read properties of undefined (reading 'isPackaged')`. + +Before any `npm run dev` / `npm run build` / `npm run dist`: + +```bash +unset ELECTRON_RUN_AS_NODE +``` + +```powershell +Remove-Item Env:ELECTRON_RUN_AS_NODE +``` + +An external terminal (Windows Terminal, cmd, plain PowerShell) doesn't have this problem; the variable is only set inside VSCode. + +## Running in dev + +```bash +npm run dev +``` + +Starts electron-vite, builds main + preload + renderer, opens an Electron window on `http://localhost:5173` (or `5174` if 5173 is taken). Closing the window ends the session. + +You'll see benign warnings in the console: + +- `ERROR:cache_util_win.cc ... Access is denied`: OneDrive sync locking Electron's user-data cache. Cosmetic. To silence, move the project out of OneDrive or set a custom user-data dir. +- `Browserslist: caniuse-lite is outdated`: cosmetic. + +## Building for distribution + +The `dist` script runs `tsc && npm run build && npm run pack`: + +```bash +unset ELECTRON_RUN_AS_NODE +npm run dist +``` + +Outputs land in `dist/`: + +- `OctoLauncher.exe`: portable single-file build +- `OctoLauncher_Installer.exe`: NSIS installer + +Targets are configured in [electron-builder.yml](electron-builder.yml). + +### Before publishing + +- The build uses `.env.production` (committed) which already points to `https://octowow.st`: no `.env` file needed for production builds. +- Code signing is not configured. Unsigned Windows builds trigger SmartScreen warnings. To sign, add a `win.certificateFile` + password (or use env-based signing) to the electron-builder config. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| `nan_scriptorigin.h ... cannot convert 'v8::Isolate *'` during `npm install` | Node 22+ breaks `nan` | Switch to Node 20 | +| `gyp ERR! find VS Could not find any Visual Studio installation` | Only VS2023+ installed | Install VS2022 Build Tools | +| `error MSB8020: The build tools for ClangCL cannot be found` | Missing LLVM component during plain `npm install` | Use `npm install --ignore-scripts` + `electron-builder install-app-deps` (see above) | +| `TypeError: Cannot read properties of undefined (reading 'isPackaged')` at launch | `ELECTRON_RUN_AS_NODE=1` set by VSCode | `unset ELECTRON_RUN_AS_NODE` | +| `Port 5173 is in use` | Prior dev server didn't exit cleanly | Ignore (vite falls back to 5174) or kill the stale process | diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6c2bd88 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 OctoWoW Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..7e9b714 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,92 @@ +# News feed + +**Note:** The launcher no longer uses a static `news.json` file. The News tab now pulls live from the OctoWoW announcements forum via the website's `/news.json` endpoint, which is backed by `ForumFeedService` on the Laravel side. To publish a news item in the launcher, post in the configured announcements forum; the launcher will pick it up within the cache TTL (default 10 minutes). There is no JSON file to edit or deploy. + +The launcher's News tab fetches `${MAIN_VITE_SERVER_URL}/news.json` and renders the entries on the landing screen. The endpoint is dynamic: it mirrors the same forum posts the website's homepage shows in its "Recent forum posts" cards, so updating the forum updates the launcher. + +## Endpoint + +`GET ${MAIN_VITE_SERVER_URL}/news.json` → `200 application/json` + +`MAIN_VITE_SERVER_URL` comes from [main/.env](.env) at build time. With the current production setup that resolves to the public site origin (e.g. `https://octowow.st/news.json`). + +The route is served by Laravel (`routes/web.php` → `news.json`) and reads from `App\Services\ForumFeedService`, which fetches the configured phpBB Atom feed (`FORUM_FEED_BASE_URL`/`FORUM_FEED_MODE`/`FORUM_FEED_FORUM_ID` in `config/customs.php` → `forum_feed`). The same service backs the homepage's `recent-forum-posts` Livewire component, so what shows in the launcher is exactly what shows on the site. + +No auth. The launcher times out after 8 seconds and validates the body against the schema below; malformed payloads surface as the "Couldn't reach the news feed" error state (with a Try again button). + +## Payload contract + +```jsonc +{ + "items": [ + { + "id": "2026-04-24-launch", // required, stable, used as React key + "title": "Welcome to the new client", // required + "date": "2026-04-24", // required, anything Date.parse() accepts + "body": "Multi-line\nbody text supported.", // required, \n preserved + "author": "example", // optional + "url": "https://example.com/changelog" // optional, must be a full URL + } + ] +} +``` + +Source of truth for the schema: [src/common/schemas.ts](src/common/schemas.ts) (`NewsItemSchema`, `NewsFeedSchema`). If you change the contract, update both ends. + +Notes: +- `items` is rendered in the order returned: sort newest-first on the server. +- `body` is rendered as plain text with `whitespace-pre-wrap`. No HTML/markdown. +- `url`, when present, becomes a "Read more" button that opens in the user's default browser via `shell.openExternal`. Skip it for inline-only posts. +- `id` should never change for an existing post (stable React keys, future bookmarking/read-state). + +## Publishing + +There is no static file to edit anymore. To change what the launcher shows, post on the forum (`FORUM_FEED_BASE_URL`, e.g. `https://octowow.st/forum`). The next launcher fetch picks it up subject to two cache layers: + +- `forum_feed.cache_ttl` (default 600 s, env `FORUM_FEED_CACHE_TTL`): Laravel server-side cache of the parsed Atom feed. +- `Cache-Control: public, max-age=120` on the `/news.json` response: short edge cache so launcher launches in a burst don't all hit Laravel. + +The launcher's react-query cache also holds for 5 minutes per session; users can hit the refresh icon in the News header to force a re-fetch (which still hits the two cache layers above). + +### Tuning what shows + +- **Which forum / mode shows**: set `FORUM_FEED_MODE` (`topics_active` | `topics` | `news` | `forum`) and, if `forum`, `FORUM_FEED_FORUM_ID` in the website container's environment. +- **How fresh**: lower `FORUM_FEED_CACHE_TTL` for fresher news at the cost of more upstream forum fetches. Pair with the route's 120-second `Cache-Control` if you also want to relax the edge cache. +- **How many items**: the route currently caps at 10; the homepage shows 3. Edit the `recent(10)` argument in `routes/web.php` → `news.json` to change the launcher cap independently of the homepage. + +## Testing + +**Confirm Laravel is serving it:** + +```bash +curl -s ${MAIN_VITE_SERVER_URL}/news.json | jq . +``` + +Expected: a `{"items": [...]}` body. An empty `items: []` means the forum feed is reachable but has nothing matching the configured mode (or the cache is still warm with an empty result; bust it by `php artisan cache:clear` inside the website container, or wait `FORUM_FEED_CACHE_TTL` seconds). + +**No items / errors:** +- `{"items": []}`: `FORUM_FEED_BASE_URL` is unset, the feed returned non-2xx, the body wasn't parseable Atom XML, or the configured forum has no posts. Check the website container's `storage/logs/laravel.log` for `ForumFeedService` warnings. +- `Couldn't reach the news feed` in the launcher: Laravel returned a 5xx (route exception, missing `ForumFeedService` binding) or the schema validator rejected the body. Check the launcher's main-process log at `%APPDATA%\octo-launcher\logs\main.log` for `Malformed news feed`. + +**End-to-end check in the launcher:** +1. Open the launcher (the News tab is the default view when no other tab is selected). +2. Click the refresh icon in the News header. +3. Entries should appear within ~1 second once Laravel + the forum cache are warm. + +## Failure modes the launcher already handles + +| Server response | UI behaviour | +| --- | --- | +| `200` with valid JSON | Renders entries | +| `200` with empty `items: []` | "No news yet: check back later." | +| `200` with malformed JSON or missing required fields | Error state + Try again. Reason logged in main-process logs (`%APPDATA%\octo-launcher\logs\main.log`). | +| `404`, `5xx`, network unreachable, > 8s timeout | Error state + Try again. | + +You don't need to ship a placeholder `news.json` to avoid 404s; the empty/error state is intentional. + +## Where the code lives + +- Main-process fetcher + schema validation: [src/main/api/routers/news.ts](src/main/api/routers/news.ts) +- Schema: [src/common/schemas.ts](src/common/schemas.ts) (`NewsItemSchema`, `NewsFeedSchema`) +- Renderer UI: [src/renderer/components/tabs/NewsTab.tsx](src/renderer/components/tabs/NewsTab.tsx) +- Router wiring: [src/main/api/root.ts](src/main/api/root.ts) (`news`) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3d4941 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# OctoLauncher + +Desktop launcher for the OctoWoW (World of Warcraft 1.12.1 private server) client. Built with Electron, React, and tRPC. + +**What it does:** +- Downloads and patches the OctoWoW game client via a manifest-based CDN updater +- Rewrites `Config.wtf` with the correct realm/patch-list on every launch +- Optionally applies binary tweaks to `WoW.exe` (FOV, far-clip, large-address flag, etc.) +- Injects client mods (VanillaFixes, DXVK, nampower, etc.) via a DLL chainloader +- Manages git-based addon installations +- Self-updates via NSIS + +--- + +## Quick start (players) + +1. Grab `OctoLauncher.exe` (portable) or `OctoLauncher_Installer.exe` from the [Releases](../../releases) page. +2. Run it and set your WoW client directory when prompted. +3. Click **Verify** to download any missing game files, then **Play**. + +No server configuration needed; the launcher connects to `octowow.st` by default. + +--- + +## Building from source + +### Prerequisites + +| Requirement | Version | Notes | +|---|---|---| +| Node.js | 20 LTS | Node 22+ breaks `dll-inject` native bindings: use Node 20 | +| VS 2022 Build Tools | C++ workload + Win SDK | `node-gyp` v10 only detects VS2017-2022 | +| Python | 3.x | Required by `node-gyp` | + +Install Node 20 with `fnm`: +```powershell +winget install Schniz.fnm +fnm install 20 +fnm default 20 +``` + +Install VS 2022 Build Tools: +```powershell +winget install Microsoft.VisualStudio.2022.BuildTools ` + --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows11SDK.22621 --includeRecommended" +``` + +### Install dependencies + +```powershell +npm install +``` + +`postinstall` rebuilds the native modules (`dll-inject`, `stormlib-node`) against the Electron ABI; expect C++ compiler output. + +### Run in development + +> **VSCode users:** The integrated terminal sets `ELECTRON_RUN_AS_NODE=1`, which crashes Electron. Unset it first: +> ```powershell +> Remove-Item Env:ELECTRON_RUN_AS_NODE +> ``` + +```powershell +npm run dev +``` + +Opens the app in a hot-reloading Electron window. The dev build points to `http://localhost:5000` by default; create a `.env` file from `.env.example` if you want to run against a local server, otherwise it falls back to `https://octowow.st`. + +### Build for distribution + +```powershell +Remove-Item Env:ELECTRON_RUN_AS_NODE +npm run dist +``` + +Outputs to `dist/`: +- `OctoLauncher.exe`: portable single-file +- `OctoLauncher_Installer.exe`: NSIS installer + +The production build uses `.env.production` (committed) which points to `https://octowow.st`. No `.env` file needed. + +--- + +## Running the dev backend + +The `server/` subdirectory is a standalone Express server that simulates the production CDN for local development. It is **not** bundled into the Electron app. + +```powershell +cd server +npm install +``` + +Create `server/.env` from `server/.env.example` and set `SOURCE_DIR` to your local WoW client directory, then: + +```powershell +npm run dev +``` + +The server listens on `http://localhost:5000` and serves: +- `GET /api/file/:version/manifest.json` +- `GET /client/:version/*`: per-file downloads +- `GET /api/addons.json` + +--- + +## Architecture overview + +Three Vite bundles tied together by tRPC over Electron IPC: + +- **Main** ([src/main/](src/main/)): Electron main process; owns all filesystem/native work and the tRPC router +- **Preload** ([src/preload/](src/preload/)): secure IPC bridge via `exposeElectronTRPC()` +- **Renderer** ([src/renderer/](src/renderer/)): React 18 + Tailwind UI; no direct Node access + +All cross-process data shapes are Zod schemas in [src/common/schemas.ts](src/common/schemas.ts). All renderer→main calls go through tRPC procedures in [src/main/api/routers/](src/main/api/routers/); never raw `ipcMain.handle`. + +--- + +## License + +MIT diff --git a/Tools/launcher/README.md b/Tools/launcher/README.md new file mode 100644 index 0000000..acf365d --- /dev/null +++ b/Tools/launcher/README.md @@ -0,0 +1,17 @@ +# Launcher build tool + +`install.ps1` downloads a portable Node.js 20 LTS into `node/` and runs the +full build pipeline (`npm install --ignore-scripts`, Electron binary install, +`electron-builder install-app-deps`, `npm run build`, `npm run pack`). + +Use this instead of a global Node install to avoid the ClangCL / Node version +issues documented in the repo root `BUILD.md`. + +```powershell +cd Tools\launcher +.\install.ps1 +``` + +Output: `dist\OctoLauncher.exe` (portable) and `dist\OctoLauncher_Installer.exe` (NSIS). + +The `node/` directory is gitignored; it is recreated by `install.ps1`. diff --git a/Tools/launcher/install.ps1 b/Tools/launcher/install.ps1 new file mode 100644 index 0000000..8e1eb77 --- /dev/null +++ b/Tools/launcher/install.ps1 @@ -0,0 +1,71 @@ +$ErrorActionPreference = 'Stop' + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$nodeDir = Join-Path $here 'node' +$mainDir = Split-Path -Parent (Split-Path -Parent $here) +$distDir = Join-Path $mainDir 'dist' + +$nodeVersion = '20.18.1' +$nodeZipName = "node-v$nodeVersion-win-x64.zip" +$nodeUrl = "https://nodejs.org/dist/v$nodeVersion/$nodeZipName" +$nodeExe = Join-Path $nodeDir "node-v$nodeVersion-win-x64\node.exe" + +if (Test-Path $nodeExe) { + Write-Host "Node.js already present at $nodeExe." -ForegroundColor Yellow +} else { + if (-not (Test-Path $nodeDir)) { New-Item -ItemType Directory -Path $nodeDir | Out-Null } + $zipPath = Join-Path $nodeDir $nodeZipName + Write-Host "Downloading Node.js $nodeVersion (~30 MB)..." -ForegroundColor Cyan + Invoke-WebRequest -Uri $nodeUrl -OutFile $zipPath -UseBasicParsing + Write-Host "Extracting to $nodeDir..." -ForegroundColor Cyan + Expand-Archive -Path $zipPath -DestinationPath $nodeDir -Force + Remove-Item $zipPath +} + +$nodeBinDir = Split-Path -Parent $nodeExe +$env:PATH = "$nodeBinDir;$env:PATH" + +Write-Host "" +Write-Host "node : $(& $nodeExe --version)" -ForegroundColor Green +Write-Host "npm : $(& (Join-Path $nodeBinDir 'npm.cmd') --version)" -ForegroundColor Green + +Write-Host "" +Write-Host "Building Electron launcher at $mainDir..." -ForegroundColor Cyan +Push-Location $mainDir +try { + if (-not (Test-Path (Join-Path $mainDir 'node_modules'))) { + Write-Host "[npm] install --ignore-scripts (JS packages only)" -ForegroundColor Cyan + & (Join-Path $nodeBinDir 'npm.cmd') install --ignore-scripts --no-audit --no-fund + if ($LASTEXITCODE -ne 0) { throw "npm install failed" } + + Write-Host "[electron] install binary" -ForegroundColor Cyan + & $nodeExe (Join-Path $mainDir 'node_modules\electron\install.js') + if ($LASTEXITCODE -ne 0) { throw "electron install failed" } + + Write-Host "[electron-builder] install-app-deps (native modules)" -ForegroundColor Cyan + & (Join-Path $mainDir 'node_modules\.bin\electron-builder.cmd') install-app-deps + if ($LASTEXITCODE -ne 0) { throw "electron-builder install-app-deps failed" } + } else { + Write-Host "node_modules already exists - skipping npm install (delete it to force reinstall)." -ForegroundColor Yellow + } + + Write-Host "[npm] run build (electron-vite)" -ForegroundColor Cyan + & (Join-Path $nodeBinDir 'npm.cmd') run build + if ($LASTEXITCODE -ne 0) { throw "npm run build failed" } + + Write-Host "[npm] run pack (electron-builder -> dist\)" -ForegroundColor Cyan + & (Join-Path $nodeBinDir 'npm.cmd') run pack + if ($LASTEXITCODE -ne 0) { throw "npm run pack failed" } +} finally { + Pop-Location +} + +Write-Host "" +Write-Host "Done." -ForegroundColor Green +if (Test-Path $distDir) { + Get-ChildItem $distDir -Filter '*.exe' | ForEach-Object { + $size = [math]::Round($_.Length / 1MB, 1) + Write-Host " $($_.FullName) ($size MB)" -ForegroundColor Green + } +} +Write-Host "" diff --git a/Tools/launcher/tracker/README.md b/Tools/launcher/tracker/README.md new file mode 100644 index 0000000..c36a61a --- /dev/null +++ b/Tools/launcher/tracker/README.md @@ -0,0 +1,66 @@ +# opentracker: OctoWow launcher torrent swarm + +BitTorrent tracker the launcher's webtorrent clients announce to. Runs +on your VPS alongside the companion update server. Tiny (~2 MB RSS), +near-zero CPU, zero disk IO after boot. + +**Why your own tracker**: public trackers (opentrackr.org, etc.) are +reliable enough for hobby swarms but add a single-point-of-failure you +don't control, and often rate-limit new info-hashes. The launcher also +announces over DHT, so your tracker is redundant with DHT, but it is +the fastest path for a fresh peer to find the swarm before DHT has +warmed up. + +## Deploy (VPS, Linux) + +SSH into the VPS, clone this repo, run: + +``` +cd Tools/launcher/tracker +chmod +x install.sh +./install.sh +``` + +`install.sh` is idempotent; re-run to update. It builds opentracker +from CVS (only distribution upstream offers), installs it under +`/opt/opentracker/bin/`, drops a hardened systemd unit, and starts the +service bound to `0.0.0.0:6969`. + +**Firewall**: open `6969/tcp` + `6969/udp`. On a typical Ubuntu VPS +with ufw: `sudo ufw allow 6969`. + +## Verify + +``` +sudo systemctl status opentracker +curl http://127.0.0.1:6969/stats?mode=tpbs # shows torrents / peers / bytes +``` + +The launcher's webtorrent client will announce to this URL the moment +a dev runs the companion server with `TRACKER_URL` set to match. + +## Wire the companion server to use this tracker + +Set the `TRACKER_URL` env var when running the companion server so +every `.torrent` it generates announces to your VPS: + +``` +TRACKER_URL=http://:6969/announce npm run server +``` + +Default is `http://127.0.0.1:6969/announce` (assumes tracker + companion +server run on the same VPS, which is the normal deployment). + +Clients pull the `.torrent` blob from the companion server; the URL +is already baked in by `create-torrent` at generation time, so no +launcher-side config needed. + +## Uninstall + +``` +sudo systemctl stop opentracker +sudo systemctl disable opentracker +sudo rm /etc/systemd/system/opentracker.service +sudo rm -rf /opt/opentracker +sudo userdel opentracker +``` diff --git a/Tools/launcher/tracker/install.sh b/Tools/launcher/tracker/install.sh new file mode 100644 index 0000000..c5654bd --- /dev/null +++ b/Tools/launcher/tracker/install.sh @@ -0,0 +1,42 @@ +set -euo pipefail + +INSTALL_PREFIX="${INSTALL_PREFIX:-/opt/opentracker}" +BUILD_DIR="$(mktemp -d)" +trap "rm -rf $BUILD_DIR" EXIT + +echo "=== Installing build deps ===" +sudo apt-get update +sudo apt-get install -y build-essential cvs zlib1g-dev + +echo "=== Fetching libowfat ===" +cd "$BUILD_DIR" +cvs -d :pserver:cvs@cvs.fefe.de:/cvs -z9 co libowfat +cd libowfat +make + +echo "=== Fetching opentracker ===" +cd "$BUILD_DIR" +cvs -d :pserver:anoncvs@cvs.fefe.de:/cvs -z9 co opentracker +cd opentracker +make FEATURES='-DWANT_V6 -DWANT_FULLSCRAPE' + +echo "=== Installing to $INSTALL_PREFIX ===" +sudo mkdir -p "$INSTALL_PREFIX/bin" +sudo cp opentracker "$INSTALL_PREFIX/bin/" +sudo cp opentracker.conf.sample "$INSTALL_PREFIX/opentracker.conf" || true +sudo useradd --system --home "$INSTALL_PREFIX" --shell /usr/sbin/nologin opentracker 2>/dev/null || true +sudo chown -R opentracker:opentracker "$INSTALL_PREFIX" + +echo "=== Installing systemd unit ===" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +sudo cp "$SCRIPT_DIR/opentracker.service" /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable opentracker +sudo systemctl start opentracker + +echo +echo "Done. Check status:" +echo " sudo systemctl status opentracker" +echo " curl http://127.0.0.1:6969/stats?mode=tpbs" +echo +echo "Don't forget to open port 6969/tcp + 6969/udp on your VPS firewall." diff --git a/Tools/launcher/tracker/opentracker.service b/Tools/launcher/tracker/opentracker.service new file mode 100644 index 0000000..b49881d --- /dev/null +++ b/Tools/launcher/tracker/opentracker.service @@ -0,0 +1,31 @@ +[Unit] +Description=opentracker: BitTorrent tracker for OctoWow launcher swarm +After=network.target + +[Service] +Type=simple +User=opentracker +Group=opentracker +WorkingDirectory=/opt/opentracker +ExecStart=/opt/opentracker/bin/opentracker -i 0.0.0.0 -p 6969 -P 6969 +Restart=on-failure +RestartSec=5 + +# Hardening: opentracker does no filesystem IO after boot, so most of +# the namespace can be locked down. +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +LockPersonality=true +MemoryDenyWriteExecute=true +SystemCallArchitectures=native + +[Install] +WantedBy=multi-user.target diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..4675a8c Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..6d788e6 Binary files /dev/null and b/build/icon.png differ diff --git a/build/installerHeader.bmp b/build/installerHeader.bmp new file mode 100644 index 0000000..7453039 Binary files /dev/null and b/build/installerHeader.bmp differ diff --git a/build/installerSidebar.bmp b/build/installerSidebar.bmp new file mode 100644 index 0000000..efd634a Binary files /dev/null and b/build/installerSidebar.bmp differ diff --git a/electron-builder.yml b/electron-builder.yml new file mode 100644 index 0000000..ed202c4 --- /dev/null +++ b/electron-builder.yml @@ -0,0 +1,48 @@ +productName: OctoLauncher +appId: st.octowow.launcher +directories: + buildResources: build + output: distprod +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!*.md' + - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,.prettierrc.cjs,dev-app-update.yml}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' + - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' + - '!*.tsbuildinfo' + - '!{tailwind.config.ts,postcss.config.cjs}' + - '!dist*/**' + - '!out/main/chunks/**' + - '!{.gitea,.github}/**' + - '!Tools/**' + - '!**/builder-debug.yml' + - '!**/electron-builder.*' + - '!.launcher/**' + - '!WTF/**' + - '!server/**' + - '!scripts/**' + - '!**/node_modules/**/{__tests__,test,tests,docs,example,examples,demo,demos,benchmark,benchmarks}/**' + - '!**/node_modules/**/*.{tsx,map,markdown}' + - '!**/node_modules/**/build/Release/obj/**' + - '!**/node_modules/**/build/Release/{*.iobj,*.ipdb,*.recipe,*.exp,*.lib,*.pdb,*.obj}' + - '!**/node_modules/**/*.{vcxproj,vcxproj.filters}' +npmRebuild: false +electronLanguages: en +extraResources: + - from: resources/aria2c.exe + to: aria2c.exe +win: + artifactName: ${productName}.${ext} + target: + - portable + - nsis +nsis: + artifactName: ${productName}_Installer.${ext} + uninstallDisplayName: ${productName} + oneClick: false + removeDefaultUninstallWelcomePage: true +publish: + - provider: generic + url: 'https://octowow.st/launcher-updates/' diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 0000000..58eb9fa --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,36 @@ +import { resolve } from 'path'; + +import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; +import react from '@vitejs/plugin-react'; +import { loadEnv } from 'vite'; + +const alias = { + '~common': resolve('src/common'), + '~main': resolve('src/main'), + '~renderer': resolve('src/renderer'), + '~build': resolve('build') +}; + +export default defineConfig(({ mode }) => { + if (mode === 'ptr') { + const realm = loadEnv(mode, process.cwd(), 'MAIN_VITE_') + .MAIN_VITE_PTR_REALMLIST; + if (!realm || realm === 'octowow.st') + throw new Error( + 'PTR build needs MAIN_VITE_PTR_REALMLIST set to a non-prod realm host' + ); + } + return { + main: { + resolve: { alias }, + plugins: [externalizeDepsPlugin()] + }, + preload: { + plugins: [externalizeDepsPlugin()] + }, + renderer: { + resolve: { alias }, + plugins: [react()] + } + }; +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..74368df --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8897 @@ +{ + "name": "octo-launcher", + "version": "1.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "octo-launcher", + "version": "1.2.0", + "hasInstallScript": true, + "dependencies": { + "@electron-toolkit/preload": "^1.0.3", + "@electron-toolkit/utils": "^1.0.2", + "@hookform/resolvers": "^3.3.2", + "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/react-query": "^4.36.1", + "@trpc/client": "^10.43.3", + "@trpc/react-query": "^10.43.3", + "@trpc/server": "^10.43.3", + "adm-zip": "^0.5.17", + "classnames": "^2.3.2", + "dll-inject": "^0.0.3", + "dompurify": "^3.4.11", + "electron-log": "^5.1.5", + "electron-trpc": "^0.5.2", + "electron-updater": "^5.3.0", + "fs-extra": "^11.1.1", + "isomorphic-git": "^1.25.0", + "lucide-react": "^0.399.0", + "node-fetch": "^2.7.0", + "react-hook-form": "^7.48.2", + "stormlib-node": "^1.3.6", + "superjson": "^1.13.3", + "zod": "^3.22.4" + }, + "devDependencies": { + "@electron-toolkit/tsconfig": "^1.0.1", + "@haaxor1689/eslint-config": "^3.0.0", + "@haaxor1689/prettier-config": "^3.0.0", + "@tailwindcss/nesting": "0.0.0-insiders.565cd3e", + "@tanstack/react-query-devtools": "^4.36.1", + "@types/adm-zip": "^0.5.8", + "@types/fs-extra": "^11.0.4", + "@types/node": "16.18.21", + "@types/node-fetch": "^2.6.9", + "@types/react": "18.0.30", + "@types/react-dom": "18.0.11", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@vitejs/plugin-react": "^3.1.0", + "autoprefixer": "^10.4.16", + "electron": "^27.0.4", + "electron-builder": "^24.6.4", + "electron-vite": "^1.0.28", + "eslint": "^8.53.0", + "eslint-config-next": "^13.5.6", + "eslint-config-prettier": "^8.10.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.33.2", + "postcss": "^8.4.31", + "postcss-nested": "^6.0.1", + "prettier": "^2.8.8", + "prettier-plugin-tailwindcss": "^0.2.8", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "^3.3.5", + "typescript": "^5.2.2", + "vite": "^4.5.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron-toolkit/preload": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@electron-toolkit/preload/-/preload-1.0.3.tgz", + "integrity": "sha512-3V/BS5Rg+Yd8lJyPA6kvHJe7zznPx9A5QrJ7AZlmnEkVWadiGSLrD87xyEaDlzgWKFuLJsyE9E4E0uNIJLWSvQ==", + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "node_modules/@electron-toolkit/tsconfig": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@electron-toolkit/tsconfig/-/tsconfig-1.0.1.tgz", + "integrity": "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==", + "dev": true, + "peerDependencies": { + "@types/node": "*" + } + }, + "node_modules/@electron-toolkit/utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@electron-toolkit/utils/-/utils-1.0.2.tgz", + "integrity": "sha512-EHqVijVPdjBJBivxJH5E/Gu6Dzd5GfoUH0Ilbtx+EQarwaeJLPLxRM5DzD54uuhs1b8TGDI7uMYmcJW2CKrijg==", + "dependencies": { + "jszip": "^3.7.1" + }, + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz", + "integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==", + "dev": true, + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/universal": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz", + "integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==", + "dev": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@haaxor1689/eslint-config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@haaxor1689/eslint-config/-/eslint-config-3.0.0.tgz", + "integrity": "sha512-iT9wOvnKHJg7nCT3CC3K/a5XlV2koVxhE7yT0LnwJ7rbHp2atecWk57bIDbFpnwTM1W7bULEPF1n7z3QOmbTlA==", + "dev": true, + "peerDependencies": { + "@haaxor1689/prettier-config": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/parser": "^5.59.2", + "eslint": "^8.39.0", + "eslint-config-next": "^13.4.1", + "eslint-config-prettier": "^8.8.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", + "prettier": "^2.8.8", + "prettier-plugin-tailwindcss": "^0.2.8", + "typescript": ">=4.9.0" + } + }, + "node_modules/@haaxor1689/prettier-config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@haaxor1689/prettier-config/-/prettier-config-3.0.0.tgz", + "integrity": "sha512-07mdtQw284u7xbZZmkHaoCx3UFBnGzaz7wL33AIyuMjdJH2nDixzHzAWpOKG0h8zcDmxFy2KoDH4nIScG08CpA==", + "dev": true, + "peerDependencies": { + "prettier": "^2.8.8", + "prettier-plugin-tailwindcss": "^0.2.8" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.2.tgz", + "integrity": "sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@next/eslint-plugin-next": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.6.tgz", + "integrity": "sha512-ng7pU/DDsxPgT6ZPvuprxrkeew3XaRf4LAT4FabaEO/hAbvVx4P7wqnqdbTdDn1kgTvsI4tpIgT4Awn/m0bGbg==", + "dev": true, + "dependencies": { + "glob": "7.1.7" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@next/eslint-plugin-next/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz", + "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tailwindcss/container-queries": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", + "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", + "peerDependencies": { + "tailwindcss": ">=3.2.0" + } + }, + "node_modules/@tailwindcss/nesting": { + "version": "0.0.0-insiders.565cd3e", + "resolved": "https://registry.npmjs.org/@tailwindcss/nesting/-/nesting-0.0.0-insiders.565cd3e.tgz", + "integrity": "sha512-WhHoFBx19TnH/c+xLwT/sxei6+4RpdfiyG3MYXfmLaMsADmVqBkF7B6lDalgZD9YdM459MF7DtxVbWkOrV7IaQ==", + "dev": true, + "dependencies": { + "postcss-nested": "^5.0.5" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/@tailwindcss/nesting/node_modules/postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", + "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "dev": true, + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz", + "integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==", + "dev": true, + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^4.36.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@trpc/client": { + "version": "10.43.3", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.43.3.tgz", + "integrity": "sha512-r/aE1l2iybJMlXCf+KWqSQpIloOqw/3jQhZD8F4VpvczWPI5fEQCmZZuPHAz2L/PiGE5K/ZwXQrJsT207ZQ2AQ==", + "funding": [ + "https://trpc.io/sponsor" + ], + "peerDependencies": { + "@trpc/server": "10.43.3" + } + }, + "node_modules/@trpc/react-query": { + "version": "10.43.3", + "resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-10.43.3.tgz", + "integrity": "sha512-yTmORp8pJ15GV8rR2QTR6bIPxk/Vbtl2vLTDz+G5GipKc+9kSRRYaVqbwzFuaPf5NOMK/JYkIsyIpitabQ1n9A==", + "funding": [ + "https://trpc.io/sponsor" + ], + "peerDependencies": { + "@tanstack/react-query": "^4.18.0", + "@trpc/client": "10.43.3", + "@trpc/server": "10.43.3", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@trpc/server": { + "version": "10.43.3", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.43.3.tgz", + "integrity": "sha512-U7HsNtgjupNvPzH6ho4rPlIXZ2t5PctOH6pmFX4jv4YJi98RjGuOhHUNhiiVb8KUw6Kuh5EHTAv7cUV+igbMuQ==", + "funding": [ + "https://trpc.io/sponsor" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/adm-zip": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz", + "integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + }, + "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 + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.21.tgz", + "integrity": "sha512-TassPGd0AEZWA10qcNnXnSNwHlLfSth8XwUaWc3gTSDmBz/rKb613Qw5qRf6o2fdRBrLbsgeC9PMZshobkuUqg==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.10", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", + "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.0.30", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.30.tgz", + "integrity": "sha512-AnME2cHDH11Pxt/yYX6r0w448BfTwQOLEhQEjCdwB7QskEI7EKtxhGUsExTQe/MsY3D9D5rMtu62WRocw9A8FA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", + "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/verror": { + "version": "1.10.9", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", + "integrity": "sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==", + "dev": true, + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "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, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true + }, + "node_modules/app-builder-lib": { + "version": "24.6.4", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.4.tgz", + "integrity": "sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg==", + "dev": true, + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.1.0", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.4.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "7zip-bin": "~5.1.1", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.5.0", + "builder-util-runtime": "9.2.1", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.5.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz", + "integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" + }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builder-util": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.5.0.tgz", + "integrity": "sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.1.1", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz", + "integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/builder-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/builder-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/builder-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/builder-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/config-file-ts": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", + "integrity": "sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ==", + "dev": true, + "dependencies": { + "glob": "^7.1.6", + "typescript": "^4.0.2" + } + }, + "node_modules/config-file-ts/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "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 + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "devOptional": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "devOptional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "optional": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==" + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dll-inject": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/dll-inject/-/dll-inject-0.0.3.tgz", + "integrity": "sha512-tWCJ/AweRzPKsghUXm3zqIcZtBhEU8L6su9IkynfudlQUiYoBaJkXrrpizY3GNRbkdcMI5sxycmjoWCOJpYDKQ==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.3.0", + "nan": "^2.11.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/dmg-builder": { + "version": "24.6.4", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.4.tgz", + "integrity": "sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw==", + "dev": true, + "dependencies": { + "app-builder-lib": "24.6.4", + "builder-util": "24.5.0", + "builder-util-runtime": "9.2.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "27.0.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-27.0.4.tgz", + "integrity": "sha512-ob29rN1mtiyAXzF8HsHd5jh8bYKd9OQDakfdOExi0F7epU97gXPHaj6JPjbBJ/vpki5d32SyKVePW4vxeNZk1A==", + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.6.4", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.4.tgz", + "integrity": "sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA==", + "dev": true, + "dependencies": { + "app-builder-lib": "24.6.4", + "builder-util": "24.5.0", + "builder-util-runtime": "9.2.1", + "chalk": "^4.1.2", + "dmg-builder": "24.6.4", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/electron-builder/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-builder/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-log": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.1.5.tgz", + "integrity": "sha512-vuq10faUAxRbILgQx7yHoMObKZDEfj7hMSZrJPsVrDNeCpV/HN11dU7QuY4UDUe055pzBxhSCB3m0+6D3Aktjw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/electron-publish": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz", + "integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.5.0", + "builder-util-runtime": "9.2.1", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/electron-publish/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/electron-publish/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-publish/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/electron-publish/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-publish/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.581", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.581.tgz", + "integrity": "sha512-6uhqWBIapTJUxgPTCHH9sqdbxIMPt7oXl0VcAL1kOtlU6aECdcMncCrX5Z7sHQ/invtrC9jUQUef7+HhO8vVFw==", + "dev": true + }, + "node_modules/electron-trpc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz", + "integrity": "sha512-lJTeTwrd/E1C3VGFvdSBQggdmdDeHGrrpnuiB1e3bQR/1f91HmprySMygs9e4i12+CLqR+RIyKTQWLKCNjy5/Q==", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "@trpc/client": ">10.0.0", + "@trpc/server": ">10.0.0", + "electron": ">19.0.0" + } + }, + "node_modules/electron-updater": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-5.3.0.tgz", + "integrity": "sha512-iKEr7yQBcvnQUPnSDYGSWC9t0eF2YbZWeYYYZzYxdl+HiRejXFENjYMnYjoOm2zxyD6Cr2JTHZhp9pqxiXuCOw==", + "dependencies": { + "@types/semver": "^7.3.6", + "builder-util-runtime": "9.1.1", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5", + "typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-1.0.28.tgz", + "integrity": "sha512-cp7nBi6do/jn5SHdL2V71WjxqZ+NXitVqn5bW+TsTEYgAfSUuYYp6INJN854kcgoOj4UrjMqA9cGRTSl79xx0Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.22.8", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "cac": "^6.7.14", + "esbuild": "^0.18.11", + "magic-string": "^0.30.1", + "picocolors": "^1.0.0" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron-vite/node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "18.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", + "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.6.tgz", + "integrity": "sha512-o8pQsUHTo9aHqJ2YiZDym5gQAMRf7O2HndHo/JZeY7TDD+W4hk6Ma8Vw54RHiBeb7OWWO5dPirQB+Is/aVQ7Kg==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "13.5.6", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prefer-arrow": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz", + "integrity": "sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=2.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "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, + "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, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "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 + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "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": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "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==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "devOptional": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "devOptional": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "devOptional": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "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", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isbinaryfile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz", + "integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-git": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.0.tgz", + "integrity": "sha512-F8X7z74gL+jN4bd6qB6a3Z0QQzonWPkiQ3nK/oFWlrc2pIwVM9Uksl3YMFh99ltswsqoCoOthgasybX08/fiGg==", + "dependencies": { + "async-lock": "^1.1.0", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/isomorphic-git/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/isomorphic-git/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "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 + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==" + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "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, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "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 + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.399.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.399.0.tgz", + "integrity": "sha512-UyTNa3djBISdzL2UktgCrESXexQXaDQWi/WsDkbw6fBFfHlapajR58WoR+gxQ4laxfEyiHmoFrEIM3V+5XOVQg==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dependencies": { + "minimist": "^1.2.5" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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 + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "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, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "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, + "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, + "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", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.8.tgz", + "integrity": "sha512-KgPcEnJeIijlMjsA6WwYgRs5rh3/q76oInqtMXBA/EMcamrcYJpyhtRhyX1ayT9hnHlHTuO8sIifHF10WuSDKg==", + "dev": true, + "engines": { + "node": ">=12.17.0" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@shufo/prettier-plugin-blade": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "prettier": ">=2.2.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*", + "prettier-plugin-twig-melody": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@shufo/prettier-plugin-blade": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "prettier-plugin-twig-melody": { + "optional": true + } + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.48.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", + "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "optional": true + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stormlib-node": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/stormlib-node/-/stormlib-node-1.3.6.tgz", + "integrity": "sha512-LBLo1fzTRd+KOAZUmsWhKNHf8gOJ+YEiu7GTXNuXE5jWXk5abdFQOpOz1FYxW+fDXdnq9sD/t2XyQqA9SJiCJw==", + "hasInstallScript": true, + "dependencies": { + "fs-extra": "^11.1.1", + "node-addon-api": "^6.1.0", + "typescript": "^5.1.3" + } + }, + "node_modules/stormlib-node/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "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, + "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", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "optionalDependencies": { + "rxjs": "*" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "optional": true + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bec8ead --- /dev/null +++ b/package.json @@ -0,0 +1,97 @@ +{ + "name": "octo-launcher", + "version": "1.3.1", + "description": "An Electron application for launching and updating the OctoWoW client", + "author": "OctoWoW", + "copyright": "Copyright © 2026 OctoWoW", + "main": "./out/main/index.js", + "scripts": { + "start": "electron-vite preview", + "dev": "electron-vite dev", + "server": "cd server && npm run dev", + "postinstall": "electron-builder install-app-deps && node scripts/scrub-native-paths.cjs", + "build": "electron-vite build", + "build:test": "electron-vite build --mode test", + "build:ptr": "electron-vite build --mode ptr", + "pack": "electron-builder --config", + "pack:ptr": "electron-builder --config electron-builder.ptr.yml", + "dist": "tsc && npm run build && npm run pack", + "dist:ptr": "tsc && npm run build:ptr && npm run pack:ptr" + }, + "dependencies": { + "@electron-toolkit/preload": "^1.0.3", + "@electron-toolkit/utils": "^1.0.2", + "@hookform/resolvers": "^3.3.2", + "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/react-query": "^4.36.1", + "@trpc/client": "^10.43.3", + "@trpc/react-query": "^10.43.3", + "@trpc/server": "^10.43.3", + "adm-zip": "^0.5.17", + "classnames": "^2.3.2", + "dll-inject": "^0.0.3", + "dompurify": "^3.4.11", + "electron-log": "^5.1.5", + "electron-trpc": "^0.5.2", + "electron-updater": "^5.3.0", + "fs-extra": "^11.1.1", + "isomorphic-git": "^1.25.0", + "lucide-react": "^0.399.0", + "node-fetch": "^2.7.0", + "react-hook-form": "^7.48.2", + "stormlib-node": "^1.3.6", + "superjson": "^1.13.3", + "zod": "^3.22.4" + }, + "devDependencies": { + "@electron-toolkit/tsconfig": "^1.0.1", + "@haaxor1689/eslint-config": "^3.0.0", + "@haaxor1689/prettier-config": "^3.0.0", + "@tailwindcss/nesting": "0.0.0-insiders.565cd3e", + "@tanstack/react-query-devtools": "^4.36.1", + "@types/adm-zip": "^0.5.8", + "@types/fs-extra": "^11.0.4", + "@types/node": "16.18.21", + "@types/node-fetch": "^2.6.9", + "@types/react": "18.0.30", + "@types/react-dom": "18.0.11", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@vitejs/plugin-react": "^3.1.0", + "autoprefixer": "^10.4.16", + "electron": "^27.0.4", + "electron-builder": "^24.6.4", + "electron-vite": "^1.0.28", + "eslint": "^8.53.0", + "eslint-config-next": "^13.5.6", + "eslint-config-prettier": "^8.10.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.33.2", + "postcss": "^8.4.31", + "postcss-nested": "^6.0.1", + "prettier": "^2.8.8", + "prettier-plugin-tailwindcss": "^0.2.8", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "^3.3.5", + "typescript": "^5.2.2", + "vite": "^4.5.0" + }, + "eslintConfig": { + "extends": "@haaxor1689/eslint-config", + "parserOptions": { + "project": [ + "./tsconfig.node.json", + "./tsconfig.web.json" + ] + }, + "rules": { + "@next/next/no-img-element": "off" + } + }, + "prettier": "@haaxor1689/prettier-config" +} diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..2cfcf6f --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + 'tailwindcss/nesting': 'postcss-nested', + 'tailwindcss': {}, + 'autoprefixer': {} + } +}; diff --git a/scripts/scrub-native-paths.cjs b/scripts/scrub-native-paths.cjs new file mode 100644 index 0000000..13aad03 --- /dev/null +++ b/scripts/scrub-native-paths.cjs @@ -0,0 +1,97 @@ + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const FILL = 0x78; + +function pathVariants(p) { + const set = new Set([p, p.replace(/\\/g, '/'), p.replace(/\//g, '\\')]); + return [...set].filter(Boolean); +} + +const root = process.cwd(); +const home = os.homedir(); +let username = ''; +try { + username = os.userInfo().username; +} catch { + username = ''; +} + +const secrets = []; +if (root.length > 2) secrets.push(...pathVariants(root)); +if (home.length > 2 && home !== root) secrets.push(...pathVariants(home)); +if (username.length >= 4) secrets.push(username); + +const needles = [...new Set(secrets)] + .filter(s => s.length > 0) + .map(s => s.toLowerCase()) + .sort((a, b) => b.length - a.length); + +function scanAndFill(buf, s, stride) { + const n = s.length; + const span = n * stride; + if (span === 0 || span > buf.length) return 0; + let hits = 0; + outer: for (let i = 0; i + span <= buf.length; i++) { + for (let j = 0; j < n; j++) { + const at = i + j * stride; + let b = buf[at]; + if (b >= 0x41 && b <= 0x5a) b += 0x20; + if (b !== s.charCodeAt(j)) continue outer; + if (stride === 2 && buf[at + 1] !== 0x00) continue outer; + } + buf.fill(FILL, i, i + span); + hits++; + i += span - 1; + } + return hits; +} + +function redact(buf) { + let hits = 0; + for (const s of needles) { + hits += scanAndFill(buf, s, 1); + hits += scanAndFill(buf, s, 2); + } + return hits; +} + +function collect(dir, out) { + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const e of entries) { + const p = path.join(dir, e.name); + if (e.isDirectory()) collect(p, out); + else if (e.isFile() && p.endsWith('.node')) out.push(p); + } +} + +const addons = []; +collect(path.join(root, 'node_modules'), addons); + +let files = 0; +let total = 0; +const redacted = []; +for (const file of addons) { + try { + const buf = fs.readFileSync(file); + const hits = redact(buf); + if (hits > 0) { + fs.writeFileSync(file, buf); + files++; + total += hits; + redacted.push(`${path.relative(root, file)} (${hits})`); + } + } catch (err) { + console.warn(`scrub-native-paths: skipped ${path.basename(file)} (${err.message})`); + } +} + +console.log(`scrub-native-paths: redacted ${total} path reference(s) across ${files} addon(s)`); +for (const r of redacted) console.log(` ${r}`); diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..426b9d6 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,7 @@ +# Copy this file to server/.env and set SOURCE_DIR to your local WoW client directory. +# This is the root of the WoW 1.12.1 client tree that the dev server will serve as CDN. +# The directory should contain WoW.exe, Data/, Interface/, etc. +SOURCE_DIR=C:\WoW\client + +# optional: path to a JSON file overriding the bundled addon sources list. +# ADDONS_SOURCES_PATH=/path/to/addons-sources.json diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..c6cfc75 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-alpine AS build + +WORKDIR /app +COPY server/package.json server/package-lock.json* ./ +RUN npm install --omit=dev --no-audit --no-fund + +COPY server/src/ ./src/ + +FROM node:22-alpine AS runtime + +RUN adduser -D -H -s /sbin/nologin manifest + +WORKDIR /app +COPY --from=build --chown=manifest:manifest /app /app + +RUN npm install --no-save --no-audit --no-fund tsx@^4.21.0 + +USER manifest +EXPOSE 5000 + +ENV SOURCE_DIR=/srv/source + +ENTRYPOINT ["node", "--import", "tsx/esm", "src/index.ts"] diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..294dca3 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1496 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "dependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.9.0", + "express": "^4.18.2", + "fs-extra": "^11.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "devDependencies": { + "tsx": "^4.21.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..f5d3161 --- /dev/null +++ b/server/package.json @@ -0,0 +1,24 @@ +{ + "name": "server", + "version": "1.0.0", + "main": "index.ts", + "type": "module", + "scripts": { + "dev": "tsx src/index.ts" + }, + "dependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.9.0", + "express": "^4.18.2", + "fs-extra": "^11.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "eslintConfig": { + "extends": "@haaxor1689/eslint-config" + }, + "prettier": "@haaxor1689/prettier-config", + "devDependencies": { + "tsx": "^4.21.0" + } +} diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml new file mode 100644 index 0000000..78fb2d0 --- /dev/null +++ b/server/pnpm-lock.yaml @@ -0,0 +1,680 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/node': + specifier: ^20.9.0 + version: 20.9.0 + express: + specifier: ^4.18.2 + version: 4.18.2 + fs-extra: + specifier: ^11.1.1 + version: 11.1.1 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@20.9.0)(typescript@5.2.2) + typescript: + specifier: ^5.2.2 + version: 5.2.2 + +packages: + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: false + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: false + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: false + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: false + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: false + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: false + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: false + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.9.0 + dev: false + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.9.0 + dev: false + + /@types/express-serve-static-core@4.17.41: + resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} + dependencies: + '@types/node': 20.9.0 + '@types/qs': 6.9.10 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: false + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.41 + '@types/qs': 6.9.10 + '@types/serve-static': 1.15.5 + dev: false + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: false + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: false + + /@types/mime@3.0.4: + resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + dev: false + + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} + dependencies: + undici-types: 5.26.5 + dev: false + + /@types/qs@6.9.10: + resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} + dev: false + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: false + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.9.0 + dev: false + + /@types/serve-static@1.15.5: + resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/mime': 3.0.4 + '@types/node': 20.9.0 + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-walk@8.3.0: + resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} + engines: {node: '>=0.4.0'} + dev: false + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: false + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: false + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /ts-node@10.9.1(@types/node@20.9.0)(typescript@5.2.2): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.9.0 + acorn: 8.11.2 + acorn-walk: 8.3.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.2.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: false + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: false diff --git a/server/src/addons-resolver.ts b/server/src/addons-resolver.ts new file mode 100644 index 0000000..c1b2ba1 --- /dev/null +++ b/server/src/addons-resolver.ts @@ -0,0 +1,285 @@ +import fs from 'fs-extra'; +import path from 'path'; + +import { defaultSources, type AddonSource } from './addons-sources.js'; + +const CACHE_TTL_MS = 60 * 60 * 1000; +const FETCH_CONCURRENCY = 8; +const FETCH_TIMEOUT_MS = 10_000; +const SOURCES_OVERRIDE_PATH = process.env.ADDONS_SOURCES_PATH ?? ''; + +export type TocData = Record; + +export type ResolvedAddon = { + name: string; + owner: string; + git: string; + branch?: string; + ref?: string; + toc?: TocData; + description?: string; + lastUpdated?: string; + stars?: number; +}; + +type CacheEntry = { at: number; data: ResolvedAddon[] }; +let cache: CacheEntry | undefined; +let inFlight: Promise | undefined; + +const normalizeColorCodes = (s: string): string => + s.replace(/\|C(?=[0-9a-fA-F]{8})/g, '|c').replace(/\|R/g, '|r'); + +const parseToc = (content: string): TocData => + content + .split('\n') + .filter(l => l.startsWith('## ')) + .map(l => l.slice(3)) + .map(l => { + const idx = l.indexOf(':'); + if (idx === -1) return null; + return [l.slice(0, idx).trim(), l.slice(idx + 1).trim()] as const; + }) + .filter((e): e is readonly [string, string] => !!e) + .reduce((acc, [k, v]) => { + acc[k] = normalizeColorCodes(v); + return acc; + }, {}); + +const fetchWithTimeout = async (url: string, init?: RequestInit) => { + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(t); + } +}; + +type RepoMeta = { + description?: string; + defaultBranch?: string; + lastUpdated?: string; + stars?: number; +}; + +type RawMeta = { + description?: string | null; + default_branch?: string; + pushed_at?: string | null; + updated_at?: string | null; + stargazers_count?: number | null; + stars_count?: number | null; +}; + +type Provider = { + apiUrl: (owner: string, repo: string) => string; + apiHeaders: () => Record; + mapMeta: (json: RawMeta) => RepoMeta; + tocUrl: (owner: string, repo: string, ref: string, name: string) => string; +}; + +const githubProvider: Provider = { + apiUrl: (o, r) => `https://api.github.com/repos/${o}/${r}`, + apiHeaders: () => ({ + Accept: 'application/vnd.github+json', + ...(process.env.GITHUB_TOKEN && { + Authorization: `Bearer ${process.env.GITHUB_TOKEN}` + }) + }), + mapMeta: j => ({ + description: j.description ?? undefined, + defaultBranch: j.default_branch, + lastUpdated: j.pushed_at ?? undefined, + stars: j.stargazers_count ?? undefined + }), + tocUrl: (o, r, ref, name) => + `https://raw.githubusercontent.com/${o}/${r}/${ref}/${name}.toc` +}; + +const GITEA_API = 'https://octowow.st/git/api/v1'; +const giteaProvider: Provider = { + apiUrl: (o, r) => `${GITEA_API}/repos/${o}/${r}`, + apiHeaders: () => ({ Accept: 'application/json' }), + mapMeta: j => ({ + description: j.description ?? undefined, + defaultBranch: j.default_branch, + lastUpdated: j.updated_at ?? undefined, + stars: j.stars_count ?? undefined + }), + tocUrl: (o, r, ref, name) => + `${GITEA_API}/repos/${o}/${r}/raw/${name}.toc?ref=${encodeURIComponent( + ref + )}` +}; + +const parseGitUrl = ( + git: string +): { owner: string; repo: string; provider: Provider } => { + const gh = git.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/); + if (gh && gh[1] && gh[2]) { + return { owner: gh[1], repo: gh[2], provider: githubProvider }; + } + const gitea = git.match(/octowow\.st\/git\/([^/]+)\/([^/]+?)(?:\.git)?$/); + if (gitea && gitea[1] && gitea[2]) { + return { owner: gitea[1], repo: gitea[2], provider: giteaProvider }; + } + throw Error(`Unsupported git URL: ${git}`); +}; + +const REQUIRED_TOC_KEYS = ['Interface']; + +const tryFetchToc = async ( + provider: Provider, + owner: string, + repo: string, + name: string, + ref: string +): Promise => { + const res = await fetchWithTimeout( + provider.tocUrl(owner, repo, ref, name) + ).catch(() => null); + if (!res?.ok) return undefined; + const parsed = parseToc(await res.text()); + return REQUIRED_TOC_KEYS.every(k => typeof parsed[k] === 'string') + ? parsed + : undefined; +}; + +const resolveOne = async (src: AddonSource): Promise => { + try { + const { owner, repo, provider } = parseGitUrl(src.git); + const name = src.name ?? repo; + + const apiRes = await fetchWithTimeout(provider.apiUrl(owner, repo), { + headers: provider.apiHeaders() + }).catch(() => null); + + let meta: RepoMeta | undefined; + if (apiRes?.ok) meta = provider.mapMeta((await apiRes.json()) as RawMeta); + + const candidates = src.ref + ? [src.ref] + : src.branch + ? [src.branch] + : [ + ...new Set( + [meta?.defaultBranch, 'main', 'master'].filter( + (b): b is string => !!b + ) + ) + ]; + + let toc: TocData | undefined; + let resolvedRef: string | undefined; + for (const ref of candidates) { + toc = await tryFetchToc(provider, owner, repo, name, ref); + if (toc) { + resolvedRef = ref; + break; + } + } + + const effectiveBranch = src.ref + ? undefined + : src.branch ?? resolvedRef ?? meta?.defaultBranch; + + let description = meta?.description ?? undefined; + const lastUpdated = meta?.lastUpdated; + const stars = meta?.stars; + + if (src.description) { + description = src.description; + if (toc) toc = { ...toc, Notes: src.description }; + } + + const result: ResolvedAddon = { name, owner, git: src.git }; + if (effectiveBranch !== undefined) result.branch = effectiveBranch; + if (src.ref !== undefined) result.ref = src.ref; + if (toc !== undefined) result.toc = toc; + if (description !== undefined) result.description = description; + if (lastUpdated !== undefined) result.lastUpdated = lastUpdated; + if (stars !== undefined) result.stars = stars; + return result; + } catch (e) { + console.error(`Failed to resolve ${src.git}:`, e); + return null; + } +}; + +const poolMap = async ( + items: T[], + concurrency: number, + fn: (item: T) => Promise +): Promise => { + const results: R[] = new Array(items.length); + let idx = 0; + const worker = async () => { + while (true) { + const i = idx++; + if (i >= items.length) return; + const item = items[i]; + if (item === undefined) return; + results[i] = await fn(item); + } + }; + await Promise.all(Array.from({ length: concurrency }, worker)); + return results; +}; + +const loadSources = async (): Promise => { + if (!SOURCES_OVERRIDE_PATH) return defaultSources; + try { + if (await fs.pathExists(SOURCES_OVERRIDE_PATH)) { + const override = (await fs.readJSON( + SOURCES_OVERRIDE_PATH + )) as AddonSource[]; + if (Array.isArray(override) && override.length > 0) { + console.log( + `Using addon sources override from ${SOURCES_OVERRIDE_PATH}` + ); + return override; + } + } + } catch (e) { + console.error( + `Failed to read override at ${SOURCES_OVERRIDE_PATH}, using defaults:`, + e + ); + } + return defaultSources; +}; + +const buildList = async (): Promise => { + const sources = await loadSources(); + console.log( + `Resolving metadata for ${sources.length} addons (concurrency=${FETCH_CONCURRENCY})...` + ); + const t0 = Date.now(); + const results = await poolMap(sources, FETCH_CONCURRENCY, resolveOne); + const ok = results.filter((r): r is ResolvedAddon => r !== null); + ok.sort((a, b) => a.name.localeCompare(b.name)); + console.log( + `Resolved ${ok.length}/${sources.length} addons in ${Date.now() - t0}ms` + ); + return ok; +}; + +export const getAddons = async (force = false): Promise => { + if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) { + return cache.data; + } + if (inFlight) return inFlight; + inFlight = buildList() + .then(data => { + cache = { at: Date.now(), data }; + return data; + }) + .finally(() => { + inFlight = undefined; + }); + return inFlight; +}; + +export const warmUp = () => { + getAddons().catch(e => console.error('Addon resolver warm-up failed:', e)); +}; diff --git a/server/src/addons-sources.ts b/server/src/addons-sources.ts new file mode 100644 index 0000000..0e2af18 --- /dev/null +++ b/server/src/addons-sources.ts @@ -0,0 +1,253 @@ +export type AddonSource = { + git: string; + branch?: string; + name?: string; + description?: string; + ref?: string; +}; + +export const defaultSources: AddonSource[] = [ + { + git: 'https://github.com/Alukarho/AI_VoiceOver.git', + description: 'Adds AI-generated voice acting to NPC quest and gossip dialogue' + }, + { + git: 'https://github.com/McPewPew/ArcHUD2.git', + description: 'Combat HUD showing health and power as arcs around your character' + }, + { + git: 'https://octowow.st/git/shaga/AtlasLoot.git', + name: 'AtlasLoot', + branch: 'main', + description: + 'Loot browser for every dungeon and raid, including the custom OctoWoW instances (Windhorn Canyon, Dragonmaw Retreat, and more)' + }, + { + git: 'https://github.com/byCFM2/Atlas-TW.git', + name: 'Atlas-CFM' + }, + { + git: 'https://github.com/Road-block/AuldLangSyne.git', + description: 'Adds personal notes to friends, ignore, and guild lists, remembered while offline' + }, + { + git: 'https://github.com/FSuhas/AutoLFM.git', + description: 'Automated "Looking For More" broadcaster for Turtle WoW dungeons and raids' + }, + { + git: 'https://github.com/shirsig/aux-addon-vanilla.git', + name: 'aux-addon', + description: 'Auction House replacement with advanced filtering and search' + }, + { git: 'https://github.com/absir/Bagshui.git' }, + { git: 'https://github.com/pepopo978/BetterCharacterStats.git', branch: 'main' }, + { git: 'https://github.com/pepopo978/BigWigs.git' }, + { + git: 'https://github.com/DBFBlackbull/BitesCookBook.git', + description: 'Tracks which items are used in cooking and what they create' + }, + { + git: 'https://github.com/brotalnia/BlizzPlates.git', + description: 'Adds castbars, debuffs, and class icons to the default Blizzard nameplates' + }, + { + git: 'https://github.com/MDGitHubRepo/CallOfElements.git', + description: 'All-in-one Shaman totem bar and totem/healing manager' + }, + { git: 'https://github.com/bhhandley/CleveRoidMacros.git' }, + { + git: 'https://github.com/Cinecom/ConsumesManager.git', + description: 'Tracks consumables and food buffs across alts, bank, and mail' + }, + { + git: 'https://github.com/Kirchlive/cursive-raid.git', + name: 'Cursive-Raid', + description: 'Raid debuff tracker with profiles and multi-curse assist (SuperWoW)' + }, + { + git: 'https://github.com/Zerf/Decursive.git', + description: 'Raid/party debuff-cleaning helper that dispels whoever needs it' + }, + { + git: 'https://github.com/DeterminedPanda/DifficultBulletinBoard.git', + description: 'Organizes LFG, profession, and hardcore chat announcements into a bulletin board' + }, + { git: 'https://github.com/Player-Doite/DoiteAuras.git' }, + { git: 'https://github.com/Stormhand-dev/DragonflightUI-Reforged.git' }, + { + git: 'https://github.com/Fiurs-Hearth/ExtraResourceBars.git', + description: 'Adds extra resource bars (mana, energy, rage) to the UI' + }, + { + git: 'https://github.com/SeVeN7000/FishingBuddy.git', + description: 'Auto-equips fishing gear and tracks catches, fish, and zone info' + }, + { git: 'https://github.com/tilare/FlightTracker.git' }, + { git: 'https://github.com/lookino/Flyout.git' }, + { + git: 'https://github.com/trumpetx/GetHead.git', + description: 'Recovers Onyxia and Nefarian heads from disenchant grief' + }, + { + git: 'https://github.com/zanthor/GNS.git', + description: 'Custom naming for Goblin Brainwashing Device specializations' + }, + { git: 'https://github.com/vatichild/guda.git', name: 'Guda' }, + { git: 'https://github.com/vatichild/GudaPlates.git' }, + { git: 'https://github.com/andresuarezschou/HCDeaths.git' }, + { + git: 'https://github.com/Arthur-Helias/InstanceJournal.git', + description: "Encounter Journal reimagined for Turtle WoW" + }, + { + git: 'https://github.com/Einherjarn/ItemRack.git', + description: 'Item set manager with quick-swap menus for inventory' + }, + { + git: 'https://github.com/CosminPOP/_LazyPig.git', + name: '_LazyPig', + description: 'Auto-dismount, auto-accept, auto-roll, and chat spam filter. /lp to configure' + }, + { git: 'https://github.com/Spartelfant/LevelRange-Turtle.git' }, + { git: 'https://github.com/tilare/MessageBox.git' }, + { + git: 'https://github.com/tdymel/ModifiedPowerAuras.git', + description: "Advanced version of Sinesther's Power Auras" + }, + { + git: 'https://github.com/tilare/ModernMapMarkers.git', + description: 'Shows dungeons, raids, world bosses, and travel routes on the world map' + }, + { + git: 'https://github.com/vegeta1k95/ModernSpellBook.git', + description: 'Retail-style spellbook UI for vanilla' + }, + { git: 'https://github.com/tilare/MovementTracker.git' }, + { + git: 'https://github.com/Dusk-92/NampowerSettings.git', + description: 'Settings panel for the Nampower spellqueue addon' + }, + { + git: 'https://github.com/BlackHobbiT/necrosis-twow.git', + name: 'Necrosis', + description: 'Warlock helper: pets, soul shards, summoning, demon timers' + }, + { + git: 'https://github.com/gnwl/NotGrid.git', + name: 'notgrid', + description: 'Grid-like compact party/raid frames with buff/debuff, aggro, and proximity tracking' + }, + { + git: 'https://github.com/zanthor/OG-RaidHelper.git', + description: 'Raid management: roles, trade distribution, soft-reserve validation' + }, + { + git: 'https://github.com/sica42/Outfitter.git', + description: 'Equipment set manager to save and quickly swap gear outfits, with Turtle mount fixes' + }, + { + git: 'https://github.com/CosminPOP/PallyPower.git', + description: 'Paladin buff and assignment manager for raids and parties' + }, + { + git: 'https://github.com/Cliencer/pfExtend.git', + description: 'pfQuest extension showing all monster drops and quest chains. /pfex' + }, + { git: 'https://github.com/shagu/pfQuest.git' }, + { git: 'https://github.com/shagu/pfQuest-turtle.git' }, + { git: 'https://github.com/shagu/pfUI.git' }, + { + git: 'https://github.com/jrc13245/pfUI-addonskinner.git', + description: 'pfUI module that re-skins other addons to match the pfUI theme' + }, + { + git: 'https://github.com/Bombg/pfUI-bettertotems.git', + description: 'pfUI module with improved Shaman totem timers' + }, + { + git: 'https://github.com/Arthur-Helias/pfUI-LocationPlus.git', + name: 'pfUI-locplus', + description: 'Adds a location panel and zone info to pfUI' + }, + { git: 'https://github.com/acid9000/PizzaWorldBuffs.git' }, + { + git: 'https://github.com/npfs666/ProcDoc.git', + description: 'Visual proc alerts with pulsing images so you never miss them' + }, + { git: 'https://github.com/SabineWren/Quiver.git' }, + { + git: 'https://github.com/hazlema/Rested.git', + description: 'Progress bar showing your rested XP while resting' + }, + { git: 'https://github.com/Otari98/Rinse.git' }, + { + git: 'https://github.com/anzz1/SellValue.git', + description: 'Shows item vendor sell value in tooltips when not at a vendor' + }, + { git: 'https://github.com/shagu/ShaguDPS.git' }, + { + git: 'https://github.com/shagu/ShaguPlates.git', + description: 'Nameplates with castbars and class colors. /splates' + }, + { git: 'https://github.com/shagu/ShaguTweaks.git' }, + { + git: 'https://github.com/shagu/ShaguTweaks-extras.git', + description: 'Extras module for ShaguTweaks (additional UI tweaks)' + }, + { git: 'https://github.com/pepopo978/SimpleActionSets.git' }, + { + git: 'https://github.com/balakethelock/SuperAPI.git', + description: 'Companion compatibility addon bridging the SuperWoW client mod\'s expanded Lua API' + }, + { + git: 'https://github.com/jrc13245/SuperMacro-turtle-SuperWoW.git', + name: 'SuperMacro', + description: 'Extended macros with long macros, keybind execution, item links, and a code editor' + }, + { git: 'https://github.com/Siventt/AttackBar.git' }, + { + git: 'https://github.com/Player-Doite/Tactica.git', + description: 'Auto-build raids: invite/gearcheck, tactics, masterloot, role sync' + }, + { + git: 'https://github.com/Otari98/Tmog.git', + description: 'Transmog item browser with collection info in tooltips' + }, + { + git: 'https://github.com/whtmst/T-RestedXP.git', + description: 'Tracks 0% and 100% rested XP thresholds' + }, + { git: 'https://github.com/sica42/TurtleCalendar.git' }, + { + git: 'https://github.com/sica42/TurtleMail.git', + description: 'Mailbox UI enhancement: bulk send, search, multi-mail' + }, + { git: 'https://github.com/tempranova/turtlerp.git', name: 'TurtleRP' }, + { git: 'https://github.com/CosminPOP/TWThreat.git' }, + { + git: 'https://github.com/RetroCro/unitscan-turtle-hc.git', + description: 'Hardcore unitscan fork for Turtle WoW that alerts on rares, elites, and dangerous mobs' + }, + { + git: 'https://github.com/whtmst/UnitXP_SP3_Addon.git', + description: 'Settings UI for the UnitXP SuperWoW client patch' + }, + { + git: 'https://github.com/tdymel/VCB.git', + description: 'Smart consolidated buff frames with extensive customization' + }, + { + git: 'https://octowow.st/git/shaga/LifeSafer_LowHealthWarning.git', + branch: 'main', + description: 'Low health and mana fullscreen flash warnings with heartbeat sound; re-enables the hidden Blizzard alert effect' + }, + { + git: 'https://github.com/Fiurs-Hearth/WIIIUI.git', + description: 'Compact custom UI replacement for Turtle WoW' + }, + { git: 'https://github.com/refaim/WIM.git' }, + { + git: 'https://github.com/Arthur-Helias/ZonesLevel.git', + description: "Shows zone level range under the title on the world map" + } +]; diff --git a/server/src/cache.ts b/server/src/cache.ts new file mode 100644 index 0000000..64cbb1e --- /dev/null +++ b/server/src/cache.ts @@ -0,0 +1,346 @@ +import crypto from 'crypto'; +import path from 'path'; + +import fs from 'fs-extra'; + +const allowedExtra = [ + '.launcher', + 'Data', + 'Errors', + 'Interface\\AddOns', + 'Logs', + 'Screenshots', + 'WDB', + 'WTF\\Account' +]; + +const vanillaFixes = ['VfPatcher.dll', 'd3d9.dll', 'dxvk.conf']; +const raidVisuals = ['patch-O.mpq']; + +const skipFiles = new Set([ + 'manifest.json', + 'manifest.json.tmp', + 'wow-client.zip', + '.gitkeep', + '.manifest-overrides.json' +]); + +const skipPatterns: RegExp[] = [ + /\.bak([.\-]|$)/, + /\.crashing(\.|$)/, + /\.torrent$/, + /^manifest\.json\./ +]; +const isSkipPattern = (file: string) => skipPatterns.some(p => p.test(file)); + +const skipDirsPosix = new Set([ + 'Interface/GlueXML', + 'Interface/FrameXML', + 'Errors', + 'Logs', + 'Screenshots', + 'WDB', + 'WTF/Account' +]); +const isSkipDir = (...filePath: string[]) => + skipDirsPosix.has(filePath.join('/')); + +type FolderTags = 'allowExtra'; +type FileTags = 'vanillaFixes' | 'raidVisuals'; + +type FileManifest = { name: string } & ( + | { type: 'dir'; files: FileManifest[]; tags?: FolderTags[] } + | { type: 'mpq'; files: FileManifest[]; hash: string; size: number } + | { + type: 'file'; + hash: string; + version?: number; + size: number; + tags?: FileTags[]; + } + | { type: 'del' } +); + +export type BuildProgress = { + state: 'idle' | 'building' | 'ready' | 'failed'; + done: number; + total: number; + currentFile: string; + startedAt: number | null; + finishedAt: number | null; + error: string | null; +}; + +export type ProgressCallback = ( + p: Pick +) => void; + +const getHash = (...filePath: string[]): Promise => + new Promise((resolve, reject) => { + const hash = crypto.createHash('sha1'); + const stream = fs.createReadStream(path.join(...filePath)); + stream.on('error', reject); + stream.on('data', (chunk: Buffer) => hash.update(chunk)); + stream.on('end', () => resolve(hash.digest('hex').toLocaleUpperCase())); + }); + +const countFiles = async ( + clientPath: string, + ...filePath: string[] +): Promise => { + let total = 0; + const dir = path.join(clientPath, ...filePath); + const files = await fs.readdir(dir); + for (const file of files.sort()) { + if (skipFiles.has(file)) continue; + if (isSkipPattern(file)) continue; + const stats = await fs.stat(path.join(dir, file)); + if (stats.isDirectory()) { + if (isSkipDir(...filePath, file)) continue; + if (file.match(/patch-./)) { + const mpqPath = path.join(dir, `${file}.mpq`); + if (await fs.pathExists(mpqPath)) total += 1; + } + total += await countFiles(clientPath, ...filePath, file); + } else { + total += 1; + } + } + return total; +}; + +export const buildCache = async ( + clientPath: string, + onProgress?: ProgressCallback +) => { + console.log('Building cache...'); + + const prevManifestPath = path.join(clientPath, 'manifest.json'); + let prevManifestMtimeMs = 0; + const prevHashByPath = new Map(); + const prevVersionByPath = new Map(); + const prevSizeByPath = new Map(); + try { + const prevStat = await fs.stat(prevManifestPath); + prevManifestMtimeMs = prevStat.mtimeMs; + const prev = await fs.readJSON(prevManifestPath); + const walk = (node: FileManifest, prefix: string[]) => { + if (node.type === 'dir' || node.type === 'mpq') { + const newPrefix = node.name ? [...prefix, node.name] : prefix; + if (node.type === 'mpq') { + const mpqKey = [...newPrefix.slice(0, -1), node.name + '.mpq'].join( + '/' + ); + prevHashByPath.set(mpqKey, node.hash); + prevSizeByPath.set(mpqKey, node.size); + } + for (const child of node.files) walk(child, newPrefix); + } else { + const key = [...prefix, node.name].join('/'); + prevHashByPath.set(key, node.hash); + prevVersionByPath.set(key, node.version); + prevSizeByPath.set(key, node.size); + } + }; + walk(prev.root, []); + console.log( + `mtime-skip: loaded ${prevHashByPath.size} cached hashes from ` + + `prior manifest (mtime=${new Date(prevManifestMtimeMs).toISOString()})` + ); + } catch (e) { + console.log( + 'mtime-skip: no usable prior manifest, full rebuild ' + + `(${(e as Error).message})` + ); + } + + const total = await countFiles(clientPath); + let done = 0; + let reused = 0; + const tick = (currentFile: string) => { + done += 1; + onProgress?.({ done, total, currentFile }); + }; + console.log(`Building cache: ${total} files to hash...`); + + const getHashCached = async ( + relPath: string, + mtimeMs: number, + ...filePath: string[] + ): Promise => { + if (prevManifestMtimeMs > 0 && mtimeMs <= prevManifestMtimeMs) { + const cached = prevHashByPath.get(relPath); + if (cached) { + reused++; + return cached; + } + } + return getHash(clientPath, ...filePath); + }; + + const buildTree = async (...filePath: string[]): Promise => { + const files = await fs.readdir(path.join(clientPath, ...filePath)); + + const patches: string[] = []; + const tree: FileManifest[] = []; + for (const file of files.sort()) { + if (skipFiles.has(file)) continue; + if (isSkipPattern(file)) continue; + + const stats = await fs.stat(path.join(clientPath, ...filePath, file)); + + if (stats.isDirectory()) { + if (isSkipDir(...filePath, file)) continue; + if (file.match(/patch-./)) { + if (raidVisuals.includes(`${file}.mpq`)) + throw new Error( + `${file}/ exists beside ${file}.mpq. Opt-in archives must stay ` + + 'whole-file: an mpq node carries no tags, so this would ' + + 'ship the patch to every player regardless of preference.' + ); + patches.push(file); + const mpqRelPath = path + .join(...filePath, `${file}.mpq`) + .split(path.sep) + .join('/'); + const mpqStat = await fs.stat( + path.join(clientPath, ...filePath, `${file}.mpq`) + ); + tree.push({ + type: 'mpq', + name: file, + files: await buildTree(...filePath, file), + size: mpqStat.size, + hash: await getHashCached( + mpqRelPath, + mpqStat.mtimeMs, + ...filePath, + `${file}.mpq` + ) + }); + tick(mpqRelPath); + } else { + const tags: FolderTags[] = []; + allowedExtra.includes(path.join(...filePath, file)) && + tags.push('allowExtra'); + tree.push({ + type: 'dir', + name: file, + files: await buildTree(...filePath, file), + tags: tags.length ? tags : undefined + }); + } + continue; + } + + if (patches.find(v => file.match(v))) continue; + + const allowModifiedPaths = new Set([ + 'WTF/Config.wtf', + 'Data/fonts.MPQ', + 'Data/sound.MPQ', + 'Data/speech.MPQ' + ]); + const fullPath = path + .join(...filePath, file) + .split(path.sep) + .join('/'); + const allowModified = + file === 'WoW.exe' || allowModifiedPaths.has(fullPath); + + const tags: FileTags[] = []; + vanillaFixes.includes(file) && tags.push('vanillaFixes'); + raidVisuals.includes(file) && tags.push('raidVisuals'); + + tree.push({ + type: 'file', + name: file, + hash: await getHashCached(fullPath, stats.mtimeMs, ...filePath, file), + version: allowModified ? stats.mtimeMs : undefined, + size: stats.size, + tags: tags.length ? tags : undefined + }); + tick(fullPath); + } + return tree; + }; + + const rootFiles = await buildTree(); + + const overridesPath = path.join(clientPath, '.manifest-overrides.json'); + try { + if (await fs.pathExists(overridesPath)) { + const ov = await fs.readJSON(overridesPath); + const dels: string[] = Array.isArray(ov.del) ? ov.del : []; + for (const relPath of dels) { + const parts = relPath.split('/').filter(Boolean); + if (parts.length === 0) continue; + const fileName = parts.pop()!; + let dirNode: FileManifest = { + type: 'dir', + name: '', + files: rootFiles + } as FileManifest; + let ok = true; + for (const seg of parts) { + if (dirNode.type !== 'dir' && dirNode.type !== 'mpq') { + ok = false; + break; + } + let child = dirNode.files.find(f => f.name === seg); + if (!child) { + child = { + type: 'dir', + name: seg, + files: [], + tags: ['allowExtra'] + }; + dirNode.files.push(child); + } + dirNode = child; + } + if (!ok || (dirNode.type !== 'dir' && dirNode.type !== 'mpq')) { + console.warn( + `manifest-overrides: del path "${relPath}" hit a non-dir ` + + `node, skipping` + ); + continue; + } + const exists = dirNode.files.some( + f => f.name === fileName && f.type === 'del' + ); + if (!exists) { + dirNode.files.push({ type: 'del', name: fileName } as FileManifest); + console.log( + `manifest-overrides: inserted {type:'del', name:'${fileName}'} ` + + `under ${parts.join('/') || ''}` + ); + } + } + } + } catch (e) { + console.warn( + `manifest-overrides: failed to apply ${overridesPath}, continuing` + + `without overrides (${(e as Error).message})` + ); + } + + const finalPath = path.join(clientPath, 'manifest.json'); + const tmpPath = path.join(clientPath, 'manifest.json.tmp'); + await fs.writeJSON(tmpPath, { + build: 3, + buildName: '3', + root: { + type: 'dir', + name: '', + files: rootFiles + } + }); + await fs.rename(tmpPath, finalPath); + if (prevManifestMtimeMs > 0) { + console.log( + `mtime-skip: reused ${reused}/${total} cached hashes ` + + `(re-hashed ${total - reused})` + ); + } +}; diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..77e015f --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,167 @@ +import path from 'path'; + +import express from 'express'; +import fs from 'fs-extra'; +import { buildCache, type BuildProgress } from './cache.js'; +import { getAddons, warmUp as warmUpAddons } from './addons-resolver.js'; + +const SourceDir = process.env.SOURCE_DIR || './client'; + +const app = express(); +const port = 5000; + +const buildProgress: BuildProgress = { + state: 'idle', + done: 0, + total: 0, + currentFile: '', + startedAt: null, + finishedAt: null, + error: null +}; + +let buildInFlight: Promise | null = null; +const ensureManifestBuilt = (): Promise => { + if (buildInFlight) return buildInFlight; + buildProgress.state = 'building'; + buildProgress.done = 0; + buildProgress.total = 0; + buildProgress.currentFile = ''; + buildProgress.startedAt = Date.now(); + buildProgress.finishedAt = null; + buildProgress.error = null; + buildInFlight = buildCache(SourceDir, p => { + buildProgress.done = p.done; + buildProgress.total = p.total; + buildProgress.currentFile = p.currentFile; + }) + .then(() => { + buildProgress.state = 'ready'; + buildProgress.finishedAt = Date.now(); + }) + .catch(e => { + buildProgress.state = 'failed'; + buildProgress.error = e instanceof Error ? e.message : String(e); + buildProgress.finishedAt = Date.now(); + buildInFlight = null; + throw e; + }); + return buildInFlight; +}; + +app.get('/api/build-status', (_req, res) => { + res.json(buildProgress); +}); + +app.get('/api/file/:version/manifest.json', async (_req, res) => { + console.log(`Fetching manifest`); + const filePath = path.join(SourceDir, 'manifest.json'); + + if (await fs.pathExists(filePath)) { + res.json(await fs.readJSON(filePath)); + return; + } + + void ensureManifestBuilt().catch(() => {}); + res.setHeader('Retry-After', '5'); + res.status(503).json({ + error: 'manifest_building', + message: + 'Manifest is being built for the first time on this server. ' + + 'Poll /api/build-status for progress; retry this endpoint when ready.', + buildProgress + }); +}); + +app.get( + '/api/file/:version/*', + async (req: express.Request<{ 0: string }>, res) => { + const filePath = req.params[0]; + console.log(`Fetching file: ${filePath}`); + + const root = path.resolve(SourceDir); + const target = path.resolve(SourceDir, filePath); + if (target !== root && !target.startsWith(root + path.sep)) { + res.status(403).end(); + return; + } + + res.sendFile(target); + } +); + +app.get('/api/addons.json', async (req, res) => { + try { + const force = req.query.refresh === '1'; + const addons = await getAddons(force); + res.json(addons); + } catch (e) { + console.error('Failed to resolve addons:', e); + res.status(500).json({ error: 'Failed to resolve addons' }); + } +}); + +const newestSourceMtime = async (dir: string): Promise => { + let newest = 0; + const entries = await fs.readdir(dir); + for (const name of entries) { + if (name === 'manifest.json' || name === 'manifest.json.tmp') continue; + const full = path.join(dir, name); + const stat = await fs.stat(full); + if (stat.isDirectory()) { + const inner = await newestSourceMtime(full); + if (inner > newest) newest = inner; + } else if (stat.mtimeMs > newest) { + newest = stat.mtimeMs; + } + } + return newest; +}; + +app.listen(port, () => { + console.log(`Server listening on port ${port}`); + warmUpAddons(); + + void (async () => { + const manifestPath = path.join(SourceDir, 'manifest.json'); + if (!fs.existsSync(manifestPath)) { + console.log(`Pre-warming manifest cache for ${SourceDir}...`); + try { + await ensureManifestBuilt(); + console.log(`Manifest cache pre-warm complete.`); + } catch (e) { + console.error( + 'Manifest pre-warm failed (will fall back to lazy build on first request):', + e + ); + } + return; + } + + buildProgress.state = 'ready'; + buildProgress.finishedAt = Date.now(); + + try { + const manifestStat = await fs.stat(manifestPath); + const newest = await newestSourceMtime(SourceDir); + if (newest > manifestStat.mtimeMs) { + console.log( + `Manifest is stale (newest source mtime ${new Date( + newest + ).toISOString()} > manifest ${new Date( + manifestStat.mtimeMs + ).toISOString()}); rebuilding in background.` + ); + ensureManifestBuilt() + .then(() => console.log('Background manifest rebuild complete.')) + .catch(e => console.error('Background manifest rebuild failed:', e)); + } else { + console.log( + `Manifest cache already on disk at ${manifestPath} and up to date; no rebuild needed.` + ); + } + } catch (e) { + console.error('Manifest staleness check failed:', e); + } + })(); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..809f12f --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "noUncheckedIndexedAccess": true, + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"], +} diff --git a/src/common/mods.ts b/src/common/mods.ts new file mode 100644 index 0000000..11bc58f --- /dev/null +++ b/src/common/mods.ts @@ -0,0 +1,214 @@ +import { z } from 'zod'; + +export const ModIdSchema = z.enum([ + 'dxvk', + 'nampower', + 'multiMonitorFix', + 'superWow', + 'transmogFix', + 'unitXp', + 'vanillaFixes', + 'vanillaHelpers' +]); +export type ModId = z.infer; + +export type ModSource = + | { + kind: 'directFile'; + url: string; + parseLatest?: 'githubRelease' | 'gitlabRelease' | 'codebergRelease'; + apiUrl?: string; + pinnedTag?: string; + assetName: string; + sha256?: string; + } + | { + kind: 'archive'; + url: string; + apiUrl?: string; + parseLatest?: 'githubRelease' | 'gitlabRelease' | 'codebergRelease'; + pinnedTag?: string; + format: 'zip' | 'tar.gz'; + extractMap: Record; + sha256?: string; + } + | { kind: 'managed' }; + +export type ModEntry = { + id: ModId; + name: string; + version: string; + description: string; + recommended?: boolean; + requires?: ModId[]; + repoUrl: string; + source: ModSource; + registerInDllsTxt?: string; + // hidden from the Mods tab, never enabled on fresh installs; existing installs keep it + disabled?: boolean; +}; + +export const MODS: ModEntry[] = [ + { + id: 'dxvk', + name: 'dxvk', + version: 'v2.7.1-1', + description: 'Enables Vulkan based rendering mode for better performance.', + recommended: true, + repoUrl: 'https://gitlab.com/Ph42oN/dxvk-gplasync', + source: { + kind: 'archive', + url: 'https://gitlab.com/Ph42oN/dxvk-gplasync/-/raw/main/releases/dxvk-gplasync-v2.7.1-1.tar.gz?ref_type=heads', + pinnedTag: 'v2.7.1-1', + format: 'tar.gz', + extractMap: { + 'dxvk-gplasync-v2.7.1-1/x32/d3d9.dll': 'd3d9.dll' + } + } + }, + { + id: 'nampower', + name: 'nampower', + version: 'v4.6.2', + description: + 'A client modification that minimizes your input lag if you have higher latency.', + repoUrl: 'https://github.com/Emyrk/nampower', + requires: ['vanillaFixes'], + source: { + kind: 'directFile', + url: 'https://github.com/Emyrk/nampower/releases/download/v4.6.2/nampower.dll', + pinnedTag: 'v4.6.2', + assetName: 'nampower.dll' + }, + registerInDllsTxt: 'nampower.dll' + }, + { + id: 'multiMonitorFix', + name: 'no1600x1200', + version: '0.2', + description: 'Fix for larger resolutions or multi monitor setups.', + repoUrl: 'https://github.com/Mates1500/VanillaMultiMonitorFix', + requires: ['vanillaFixes'], + source: { + kind: 'archive', + url: 'https://github.com/Mates1500/VanillaMultiMonitorFix/releases/download/0.2/release.zip', + apiUrl: + 'https://api.github.com/repos/Mates1500/VanillaMultiMonitorFix/releases/latest', + parseLatest: 'githubRelease', + pinnedTag: '0.2', + format: 'zip', + extractMap: { + 'VanillaMultiMonitorFix.dll': 'VanillaMultiMonitorFix.dll' + } + }, + registerInDllsTxt: 'VanillaMultiMonitorFix.dll' + }, + { + id: 'superWow', + name: 'SuperWoW', + version: '2.2', + description: + 'Extends the client Lua API with unit GUIDs and other data many addons rely on.', + repoUrl: 'https://github.com/balakethelock/SuperWoW', + requires: ['vanillaFixes'], + source: { + kind: 'archive', + url: 'https://github.com/balakethelock/SuperWoW/releases/download/Release/SuperWoW.release.2.2.zip', + apiUrl: + 'https://api.github.com/repos/balakethelock/SuperWoW/releases/latest', + parseLatest: 'githubRelease', + pinnedTag: '2.2', + format: 'zip', + extractMap: { + 'SuperWoWhook.dll': 'SuperWoWhook.dll' + } + }, + registerInDllsTxt: 'SuperWoWhook.dll', + // disabled 2026-08-08 pending distribution permission; delete this line to re-enable + disabled: true + }, + { + id: 'transmogFix', + name: 'transmogFix', + version: 'v0.7.0', + description: + "A client-side fix that eliminates frame drops caused by the server's transmogrification durability workaround.", + repoUrl: 'https://codeberg.org/MarcelineVQ/WeirdUtils', + requires: ['vanillaFixes'], + source: { + kind: 'directFile', + url: 'https://codeberg.org/MarcelineVQ/WeirdUtils/releases/download/v0.7.0/transmogfix.dll', + pinnedTag: 'v0.7.0', + assetName: 'transmogfix.dll' + }, + registerInDllsTxt: 'transmogfix.dll' + }, + { + id: 'unitXp', + name: 'unitXp', + version: 'v89', + description: 'An attempt to make Vanilla 1.12 modern.', + repoUrl: 'https://codeberg.org/konaka/UnitXP_SP3', + requires: ['vanillaFixes'], + source: { + kind: 'archive', + url: 'https://codeberg.org/konaka/UnitXP_SP3/releases/download/v89/UnitXP_SP3%20v89.zip', + pinnedTag: 'v89', + format: 'zip', + extractMap: { + 'UnitXP_SP3.dll': 'UnitXP_SP3.dll' + } + }, + registerInDllsTxt: 'UnitXP_SP3.dll' + }, + { + id: 'vanillaFixes', + name: 'vanillaFixes', + version: 'v1.5.3', + description: + 'A client modification that eliminates stutter and animation lag.', + recommended: true, + repoUrl: 'https://github.com/hannesmann/vanillafixes', + source: { + kind: 'archive', + url: 'https://github.com/hannesmann/vanillafixes/releases/download/v1.5.3/vanillafixes-1.5.3.zip', + apiUrl: + 'https://api.github.com/repos/hannesmann/vanillafixes/releases/latest', + parseLatest: 'githubRelease', + pinnedTag: 'v1.5.3', + format: 'zip', + extractMap: { + 'VfPatcher.dll': 'VfPatcher.dll', + 'VanillaFixes.exe': 'VanillaFixes.exe' + } + } + }, + { + id: 'vanillaHelpers', + name: 'vanillaHelpers', + version: 'v1.1.2', + description: + 'Utility library that might be required by other patches and addons.', + repoUrl: 'https://github.com/isfir/VanillaHelpers', + requires: ['vanillaFixes'], + source: { + kind: 'directFile', + url: 'https://github.com/isfir/VanillaHelpers/releases/download/v1.1.2/VanillaHelpers.dll', + apiUrl: + 'https://api.github.com/repos/isfir/VanillaHelpers/releases/latest', + parseLatest: 'githubRelease', + pinnedTag: 'v1.1.2', + assetName: 'VanillaHelpers.dll' + }, + registerInDllsTxt: 'VanillaHelpers.dll' + } +]; + +export const getMod = (id: ModId): ModEntry | undefined => + MODS.find(m => m.id === id); + +const NOT_DEFAULT_ENABLED: ModId[] = []; + +export const DEFAULT_ENABLED_MODS: ModId[] = MODS.filter( + m => !m.disabled && !NOT_DEFAULT_ENABLED.includes(m.id) +).map(m => m.id); diff --git a/src/common/schemas.ts b/src/common/schemas.ts new file mode 100644 index 0000000..edc10df --- /dev/null +++ b/src/common/schemas.ts @@ -0,0 +1,149 @@ +import { z } from 'zod'; + +const f = { + boolean: (defaultValue?: boolean) => + z.boolean().nullish().default(!!defaultValue), + number: (defaultValue?: number, val?: (v: z.ZodNumber) => z.ZodNumber) => + z.preprocess( + v => + v === '' || v === undefined + ? defaultValue ?? null + : typeof v === 'string' + ? Number(v) + : v, + (val?.(z.number()) ?? z.number()).nullish() + ) +}; + +export const ConfigWtfSchema = z.object({ + vanillaFixes: f.boolean(), + raidVisuals: f.boolean(), + largeAddress: f.boolean(true), + nameplateRange: f.number(41), + alwaysAutoLoot: f.boolean(), + fieldOfView: f.number(110), + farClip: f.number(777), + frillDistance: f.number(70), + cameraDistance: f.number(50), + soundInBackground: f.boolean(true) +}); +export type ConfigWtfSchema = z.infer; + +export const ModStateSchema = z.object({ + enabled: z.boolean().default(false), + installedVersion: z.string().optional(), + installedFiles: z.array(z.string()).default([]), + ignoreUpdates: z.boolean().default(false) +}); +export type ModState = z.infer; + +export const HardwareInfoSchema = z.object({ + totalRamMb: z.number(), + cpuCores: z.number(), + cpuModel: z.string(), + gpuModel: z.string(), + vramMb: z.number().nullable(), + vramSource: z.enum(['registry', 'wmi', 'none']), + detectedAt: z.string(), + schemaVersion: z.number() +}); +export type HardwareInfo = z.infer; + +export const PreferencesSchema = z.object({ + isPortable: z.boolean().optional(), + server: z.enum(['live', 'ptr']).default('live'), + clientDir: z.string().optional(), + version: z.string().optional(), + lastPatchedLauncherVersion: z.string().optional(), + expectedPatchedWowHash: z.string().optional(), + minimizeToTrayOnPlay: f.boolean(true), + cleanWdb: f.boolean(true), + shareDownloads: f.boolean(true), + locale: z + .enum(['enUS', 'deDE', 'zhCN', 'esES', 'ptBR', 'ruRU']) + .default('enUS'), + localePatchLetter: z.string().optional(), + localePatchLocale: z.string().optional(), + patchedLocale: z.string().optional(), + syncedTorrentHash: z.string().optional(), + activeTorrentHash: z.string().optional(), + activeClientDir: z.string().optional(), + raidVisualsHash: z.string().optional(), + clientPatchHash: z.string().optional(), + vmmfWrittenIndex: z.number().int().nonnegative().optional(), + lastWrittenResolution: z.string().optional(), + rememberPosition: f.boolean(), + windowPosition: z + .object({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number() + }) + .nullish(), + config: ConfigWtfSchema.default({}), + mods: z.record(ModStateSchema).default({}), + hardware: HardwareInfoSchema.optional(), + farClipUserSet: z.boolean().optional() +}); +export type PreferencesSchema = z.infer; + +export const TocDataSchema = z.object({ + Interface: z.string(), + Title: z.string(), + Author: z.string(), + Notes: z.string(), + Version: z.string(), + Dependencies: z.string().optional(), + OptionalDeps: z.string().optional() +}); + +export type TocData = z.infer; + +export const AddonDataSchema = z.object({ + status: z.enum([ + 'available', + 'fetching', + 'unknown', + 'upToDate', + 'outOfDate', + 'downloading', + 'invalid' + ]), + git: z.string().optional(), + toc: TocDataSchema.optional(), + description: z.string().optional(), + error: z.string().optional(), + branch: z.string().optional(), + ref: z.string().optional(), + folder: z.string(), + progress: z.string().optional(), + preview: z.string().optional() +}); + +export type AddonData = z.infer; + +export const NewsItemSchema = z.object({ + id: z.string(), + title: z.string(), + date: z.string(), + body: z.string(), + url: z.string().url().optional(), + author: z.string().nullish() +}); +export type NewsItem = z.infer; + +export const NewsFeedSchema = z.object({ + items: z.array(NewsItemSchema) +}); +export type NewsFeed = z.infer; + +export const ForumAnnouncementSchema = z.object({ + id: z.string(), + title: z.string(), + author: z.string().nullish(), + date: z.string(), + url: z.string().url(), + html: z.string() +}); +export type ForumAnnouncement = z.infer; diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 0000000..340c26a --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,82 @@ +type Path = readonly (string | number)[]; + +const isUnsafeKey = (key: string | number) => + key === '__proto__' || key === 'constructor' || key === 'prototype'; + +export const nestedGet = (object: unknown, path: Path) => + path.reduce( + (obj, key) => (isUnsafeKey(key) ? undefined : obj?.[key]), + object + ) as T; + +export const nestedSet = (obj: any, path: Path, value: unknown) => { + const [key, ...rest] = path; + if (isUnsafeKey(key)) return; + + if (path.length === 1) { + obj[key] = value; + return; + } + + if (obj[key] === undefined) { + obj[key] = typeof rest[0] === 'number' ? [] : {}; + } + + nestedSet(obj[key], rest as never, value); +}; + +export const asyncReduce = async ( + arr: T[], + reducer: (acc: U, cur: T) => Promise, + init: U +): Promise => { + let acc: U = init; + for (const i of arr) acc = await reducer(acc, i); + return acc; +}; + +export const asyncMap = async ( + arr: T[], + map: (cur: T) => Promise +): Promise => { + const acc: U[] = []; + for (const i of arr) acc.push(await map(i)); + return acc; +}; + +export const isNotUndef = (obj: T): obj is Exclude => + obj !== undefined; + +export const formatFileSize = (bytes: number, decimals = 2) => { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${parseFloat(size.toFixed(decimals))} ${units[unitIndex]}`; +}; + +export const formatDuration = (remaining: number) => { + const hours = Math.floor(remaining / 3600); + const minutes = Math.floor((remaining % 3600) / 60); + const seconds = Math.floor(remaining % 60); + + return `${hours ? `${hours}h ` : ''}${ + minutes ? `${minutes}m ` : '' + }${seconds}s`; +}; + +export const omit = ( + obj: T, + keys: K[] +): Omit => { + const result = { ...obj }; + keys.forEach(key => { + delete result[key]; + }); + return result; +}; diff --git a/src/main/api/root.ts b/src/main/api/root.ts new file mode 100644 index 0000000..f4b7e8b --- /dev/null +++ b/src/main/api/root.ts @@ -0,0 +1,26 @@ +import { createTRPCRouter } from './trpc'; +import { addonsRouter } from './routers/addonts'; +import { launcherRouter } from './routers/launcher'; +import { updaterRouter } from './routers/updater'; +import { patcherRouter } from './routers/patcher'; +import { generalRouter } from './routers/general'; +import { preferencesRouter } from './routers/preferences'; +import { newsRouter } from './routers/news'; +import { forumRouter } from './routers/forum'; +import { modsRouter } from './routers/mods'; +import { selfUpdaterRouter } from './routers/selfUpdater'; + +export const appRouter = createTRPCRouter({ + addons: addonsRouter, + general: generalRouter, + preferences: preferencesRouter, + launcher: launcherRouter, + patcher: patcherRouter, + updater: updaterRouter, + news: newsRouter, + forum: forumRouter, + mods: modsRouter, + selfUpdater: selfUpdaterRouter +}); + +export type AppRouter = typeof appRouter; diff --git a/src/main/api/routers/addonts.ts b/src/main/api/routers/addonts.ts new file mode 100644 index 0000000..e4beec8 --- /dev/null +++ b/src/main/api/routers/addonts.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import Addons from '~main/modules/addons'; +import { AddonDataSchema } from '~common/schemas'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const addonsRouter = createTRPCRouter({ + verify: publicProcedure.mutation(() => { + Addons.verify(); + }), + update: publicProcedure + .input(z.object({ toUpdate: z.array(z.string()).optional() })) + .mutation(({ input }) => Addons.update(input.toUpdate)), + install: publicProcedure + .input(AddonDataSchema) + .mutation(({ input }) => Addons.install(input)), + remove: publicProcedure + .input(z.object({ toDelete: z.array(z.string()) })) + .mutation(({ input }) => Addons.remove(input.toDelete)), + checkGitUrl: publicProcedure + .input(z.string()) + .query(({ input }) => Addons.checkGitUrl(input)), + observe: publicProcedure.subscription(() => Addons.observe()) +}); diff --git a/src/main/api/routers/forum.ts b/src/main/api/routers/forum.ts new file mode 100644 index 0000000..330f87c --- /dev/null +++ b/src/main/api/routers/forum.ts @@ -0,0 +1,47 @@ +import fetch from 'node-fetch'; +import Logger from 'electron-log/main'; + +import { + ForumAnnouncementSchema, + type ForumAnnouncement +} from '~common/schemas'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +const FETCH_TIMEOUT_MS = 8_000; + +const fetchLatestAnnouncement = async (): Promise => { + const url = `${ + import.meta.env.MAIN_VITE_FORUM_URL || 'https://octowow.st' + }/forum/octonews.php?forum=35&mode=full`; + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const res = await fetch(url, { signal: controller.signal }); + if (!res.ok) throw Error(`HTTP ${res.status}`); + const json = (await res.json()) as unknown; + if (!json || typeof json !== 'object' || !('id' in json)) return null; + const parsed = ForumAnnouncementSchema.safeParse(json); + if (!parsed.success) { + Logger.error( + 'Forum announcement failed schema validation', + parsed.error.flatten() + ); + throw Error('Malformed forum announcement'); + } + return parsed.data; + } finally { + clearTimeout(t); + } +}; + +export const forumRouter = createTRPCRouter({ + latestAnnouncement: publicProcedure.query(async () => { + try { + return await fetchLatestAnnouncement(); + } catch (e) { + Logger.error('Failed to fetch forum announcement', e); + throw e; + } + }) +}); diff --git a/src/main/api/routers/general.ts b/src/main/api/routers/general.ts new file mode 100644 index 0000000..797d5d1 --- /dev/null +++ b/src/main/api/routers/general.ts @@ -0,0 +1,88 @@ +import path from 'node:path'; + +import { app, dialog, shell } from 'electron'; +import Logger from 'electron-log/main'; +import { z } from 'zod'; + +import { mainWindow } from '~main/index'; +import Preferences from '~main/modules/preferences'; +import { + addDefenderExclusions, + detectAntivirusBlocks +} from '~main/modules/defender'; +import { detectHardware, recommendFarClip } from '~main/modules/hardware'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const generalRouter = createTRPCRouter({ + appVersion: publicProcedure.query(() => app.getVersion()), + hardware: publicProcedure.query(() => { + const hardware = Preferences.data.hardware ?? null; + return { hardware, recommendedFarClip: recommendFarClip(hardware) }; + }), + redetectHardware: publicProcedure.mutation(async () => { + const hardware = await detectHardware(); + Preferences.data = { hardware }; + return { hardware, recommendedFarClip: recommendFarClip(hardware) }; + }), + quit: publicProcedure.mutation(() => app.quit()), + minimize: publicProcedure.mutation(() => mainWindow?.minimize()), + openLink: publicProcedure + .input(z.string().url()) + .mutation(({ input }) => shell.openExternal(input)), + openInstallFolder: publicProcedure.mutation(() => { + // Explorer needs native separators; a stored forward-slash path fails to open. + const dir = Preferences.data.clientDir; + if (dir) shell.openPath(path.normalize(dir)); + }), + openLogFile: publicProcedure.mutation(() => { + const file = Logger.transports.file.getFile().path; + shell.openPath(path.normalize(file)); + }), + addDefenderExclusion: publicProcedure.mutation(() => addDefenderExclusions()), + antivirusBlocks: publicProcedure.query(() => detectAntivirusBlocks()), + filePicker: publicProcedure + .input( + z.object({ + title: z.string().optional(), + message: z.string().optional(), + filters: z + .array( + z.object({ + name: z.string(), + extensions: z.array(z.string()) + }) + ) + .optional(), + properties: z + .array( + z.enum([ + 'openDirectory', + 'openFile', + 'multiSelections', + 'showHiddenFiles', + 'createDirectory', + 'promptToCreate', + 'noResolveAliases', + 'treatPackageAsDirectory', + 'dontAddToRecent' + ]) + ) + .optional() + }) + ) + .mutation(async ({ input }) => { + if (!mainWindow) return { canceled: true } as const; + const { canceled, filePaths } = await dialog.showOpenDialog( + mainWindow, + input + ); + + return canceled + ? ({ canceled: true } as const) + : ({ + canceled: false, + path: filePaths as [string, ...string[]] + } as const); + }) +}); diff --git a/src/main/api/routers/launcher.ts b/src/main/api/routers/launcher.ts new file mode 100644 index 0000000..5482c69 --- /dev/null +++ b/src/main/api/routers/launcher.ts @@ -0,0 +1,172 @@ +import path from 'path'; +import { spawn } from 'child_process'; + +import fs from 'fs-extra'; +import Logger from 'electron-log/main'; + +import Preferences from '~main/modules/preferences'; +import Mods from '~main/modules/mods'; +import { mainWindow } from '~main/index'; +import Updater, { isGameRunning } from '~main/modules/updater'; +import { + patchConfig, + patchExecutable, + ensureDxvkConf +} from '~main/modules/patcher'; +import { removeLegacyLocalePatches } from '~main/modules/localePatch'; +import { syncVanillaFixesCache } from '~main/modules/dllsTxt'; +import { stopSeeding } from '~main/modules/aria2'; +import { minimizeToTray, restoreFromTray } from '~main/modules/tray'; +import { getMod } from '~common/mods'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +const chainloaderNeeded = async (clientDir: string): Promise => { + const installed = Mods.status.mods.filter(r => r.installedVersion); + if (installed.some(r => r.id === 'vanillaFixes')) return true; + if (installed.some(r => getMod(r.id)?.requires?.includes('vanillaFixes'))) + return true; + + const dllsPath = path.join(clientDir, 'dlls.txt'); + if (await fs.pathExists(dllsPath)) { + const raw = await fs.readFile(dllsPath, 'utf8'); + return raw.split(/\r?\n/).some(l => l.trim() && !l.trim().startsWith('#')); + } + return false; +}; + +type StartResult = { ok: boolean; error?: string }; + +const delay = (ms: number) => new Promise(r => setTimeout(r, ms)); + +let starting = false; + +export const launcherRouter = createTRPCRouter({ + start: publicProcedure.mutation(async (): Promise => { + if (starting) return { ok: false, error: 'The game is already launching.' }; + starting = true; + try { + const { cleanWdb, minimizeToTrayOnPlay, clientDir } = Preferences.data; + if (!clientDir) return { ok: false, error: 'No game folder is set.' }; + + const exePath = path.join(clientDir, 'WoW.exe'); + if (!(await fs.pathExists(exePath))) + return { + ok: false, + error: 'WoW.exe was not found in the game folder.' + }; + if (await isGameRunning(exePath)) + return { ok: false, error: 'WoW is already running.' }; + + if (Mods.status.dirty) + return { + ok: false, + error: 'You have unapplied mod changes. Click Apply first.' + }; + + stopSeeding(); + + if (cleanWdb) { + Logger.log('Cleaning up WDB...'); + await fs.remove(path.join(clientDir, 'WDB')); + } + + Logger.log('Syncing preferred monitor...'); + await Mods.verify(); + + Logger.log('Checking Config.wtf...'); + await patchConfig(); + await ensureDxvkConf(clientDir); + + await removeLegacyLocalePatches(clientDir); + + if (Preferences.data.patchedLocale !== Preferences.data.locale) { + Logger.log( + `Applying the client language (${Preferences.data.locale})...` + ); + try { + await patchExecutable(); + await patchConfig(true); + await Updater.recordPatchedWow(); + if (!cleanWdb) + await fs.remove(path.join(clientDir, 'WDB')).catch(() => {}); + } catch (e) { + Logger.error( + 'Could not apply the client language; launching with the previous one', + e + ); + } + } + + const loaderPath = path.join(clientDir, 'VanillaFixes.exe'); + const needsLoader = await chainloaderNeeded(clientDir); + const useLoader = needsLoader && (await fs.pathExists(loaderPath)); + if (useLoader) await syncVanillaFixesCache(clientDir); + if (needsLoader && !useLoader) + Logger.warn( + 'VanillaFixes.exe is missing but mods/dlls.txt expect a chainloader; ' + + 'launching WoW.exe directly (mods will not load).' + ); + + Logger.log( + useLoader ? 'Launching via VanillaFixes...' : `Launching ${exePath}...` + ); + const child = useLoader + ? spawn(loaderPath, ['WoW.exe'], { + cwd: clientDir, + detached: !minimizeToTrayOnPlay + }) + : spawn(exePath, { + cwd: clientDir, + detached: !minimizeToTrayOnPlay + }); + + try { + await new Promise((resolve, reject) => { + child.once('spawn', resolve); + child.once('error', reject); + }); + } catch (e) { + Logger.error('Failed to launch the game', e); + const message = e instanceof Error ? e.message : String(e); + return { ok: false, error: `Failed to launch the game: ${message}` }; + } + + child.on('error', e => Logger.error('Game process error', e)); + + if (!minimizeToTrayOnPlay) { + mainWindow?.close(); + return { ok: true }; + } + + minimizeToTray(); + if (useLoader) { + void (async () => { + try { + const started = Date.now(); + while ( + Date.now() - started < 30_000 && + !(await isGameRunning(exePath)) + ) + await delay(1000); + while (await isGameRunning(exePath)) await delay(3000); + } finally { + Logger.log('WoW stopped'); + restoreFromTray(); + } + })(); + } else { + child.on('exit', () => { + Logger.log('WoW stopped'); + restoreFromTray(); + }); + } + return { ok: true }; + } catch (e) { + Logger.error('Failed to start the game', e); + return { ok: false, error: e instanceof Error ? e.message : String(e) }; + } finally { + starting = false; + } + }) +}); diff --git a/src/main/api/routers/mods.ts b/src/main/api/routers/mods.ts new file mode 100644 index 0000000..8208bda --- /dev/null +++ b/src/main/api/routers/mods.ts @@ -0,0 +1,38 @@ +import path from 'path'; + +import { z } from 'zod'; + +import Mods from '~main/modules/mods'; +import Preferences from '~main/modules/preferences'; +import { isGameRunning } from '~main/modules/updater'; +import { ModIdSchema } from '~common/mods'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const modsRouter = createTRPCRouter({ + list: publicProcedure.query(() => Mods.status), + verify: publicProcedure.mutation(() => Mods.verify()), + toggle: publicProcedure + .input(z.object({ id: ModIdSchema, enabled: z.boolean() })) + .mutation(({ input }) => Mods.toggle(input.id, input.enabled)), + toggleCustom: publicProcedure + .input(z.object({ name: z.string(), enabled: z.boolean() })) + .mutation(({ input }) => Mods.toggleCustom(input.name, input.enabled)), + addCustomDll: publicProcedure + .input(z.object({ path: z.string() })) + .mutation(({ input }) => Mods.addCustomDll(input.path)), + setIgnoreUpdates: publicProcedure + .input(z.object({ id: ModIdSchema, ignore: z.boolean() })) + .mutation(({ input }) => Mods.setIgnoreUpdates(input.id, input.ignore)), + applyAll: publicProcedure.mutation(() => Mods.applyAll()), + repair: publicProcedure.mutation(async () => { + const clientDir = Preferences.data?.clientDir; + if (clientDir) { + const exePath = path.join(clientDir, 'WoW.exe'); + if (await isGameRunning(exePath)) + throw new Error('Please close WoW first before verifying files.'); + } + return Mods.applyAll({ repairOnly: true }); + }), + observe: publicProcedure.subscription(() => Mods.observe()) +}); diff --git a/src/main/api/routers/news.ts b/src/main/api/routers/news.ts new file mode 100644 index 0000000..67c97ba --- /dev/null +++ b/src/main/api/routers/news.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; +import fetch from 'node-fetch'; +import Logger from 'electron-log/main'; + +import { NewsFeedSchema, type NewsItem } from '~common/schemas'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +const FETCH_TIMEOUT_MS = 8_000; + +// Boards octonews.php exposes as a list: 2 = Announcements, 4 = Patch Notes. +const FEED_FORUMS = [2, 4]; + +const fetchNews = async (forum: number): Promise => { + const f = FEED_FORUMS.includes(forum) ? forum : 2; + const url = `${ + import.meta.env.MAIN_VITE_FORUM_URL || 'https://octowow.st' + }/forum/octonews.php?mode=list&forum=${f}&limit=5`; + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const res = await fetch(url, { signal: controller.signal }); + if (!res.ok) throw Error(`HTTP ${res.status}`); + const parsed = NewsFeedSchema.safeParse(await res.json()); + if (!parsed.success) { + Logger.error( + 'News feed failed schema validation', + parsed.error.flatten() + ); + throw Error('Malformed news feed'); + } + return parsed.data.items; + } finally { + clearTimeout(t); + } +}; + +export const newsRouter = createTRPCRouter({ + list: publicProcedure + .input(z.object({ forum: z.number() }).optional()) + .query(async ({ input }) => { + try { + return await fetchNews(input?.forum ?? 2); + } catch (e) { + Logger.error('Failed to fetch news', e); + throw e; + } + }) +}); diff --git a/src/main/api/routers/patcher.ts b/src/main/api/routers/patcher.ts new file mode 100644 index 0000000..8a34ad1 --- /dev/null +++ b/src/main/api/routers/patcher.ts @@ -0,0 +1,15 @@ +import { patchConfig, patchExecutable } from '~main/modules/patcher'; +import Preferences from '~main/modules/preferences'; +import Updater from '~main/modules/updater'; +import { getClientVersion } from '~main/utils'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const patcherRouter = createTRPCRouter({ + apply: publicProcedure.mutation(async () => { + await patchExecutable(); + await patchConfig(true); + await Updater.recordPatchedWow(); + Preferences.data = { version: await getClientVersion() }; + }) +}); diff --git a/src/main/api/routers/preferences.ts b/src/main/api/routers/preferences.ts new file mode 100644 index 0000000..c3ef5cb --- /dev/null +++ b/src/main/api/routers/preferences.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +import { PreferencesSchema } from '~common/schemas'; +import Preferences from '~main/modules/preferences'; +import Updater from '~main/modules/updater'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const preferencesRouter = createTRPCRouter({ + get: publicProcedure.output(PreferencesSchema).query(() => Preferences.data), + set: publicProcedure + .input(PreferencesSchema.partial()) + .mutation(async ({ input }) => { + // Language change no longer touches the game folder; the exe is re-patched on + // the next Play (launcher router), so this stays network-free and can't fail. + Preferences.data = input; + if (input.shareDownloads !== undefined) void Updater.refreshSeeding(); + return Preferences.data; + }), + isValidClientDir: publicProcedure + .input(z.string().optional()) + .query(({ input }) => Preferences.isValidClientDir(input)) +}); diff --git a/src/main/api/routers/selfUpdater.ts b/src/main/api/routers/selfUpdater.ts new file mode 100644 index 0000000..4653d5f --- /dev/null +++ b/src/main/api/routers/selfUpdater.ts @@ -0,0 +1,8 @@ +import SelfUpdater from '~main/modules/selfUpdater'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const selfUpdaterRouter = createTRPCRouter({ + observe: publicProcedure.subscription(() => SelfUpdater.observe()), + install: publicProcedure.mutation(() => SelfUpdater.triggerInstall()) +}); diff --git a/src/main/api/routers/updater.ts b/src/main/api/routers/updater.ts new file mode 100644 index 0000000..2f71153 --- /dev/null +++ b/src/main/api/routers/updater.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +import Updater from '~main/modules/updater'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; + +export const updaterRouter = createTRPCRouter({ + verify: publicProcedure.mutation(() => Updater.verify()), + syncRaidVisuals: publicProcedure.mutation(() => Updater.syncRaidVisuals()), + update: publicProcedure + .input(z.boolean().optional()) + .mutation(async ({ input }) => Updater.update(input)), + observe: publicProcedure.subscription(() => Updater.observe()) +}); diff --git a/src/main/api/trpc.ts b/src/main/api/trpc.ts new file mode 100644 index 0000000..9c828d8 --- /dev/null +++ b/src/main/api/trpc.ts @@ -0,0 +1,18 @@ +import { initTRPC } from '@trpc/server'; +import superjson from 'superjson'; +import { ZodError } from 'zod'; + +const t = initTRPC.create({ + transformer: superjson, + errorFormatter: ({ shape, error }) => ({ + ...shape, + data: { + ...shape.data, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null + } + }) +}); + +export const createTRPCRouter = t.router; + +export const publicProcedure = t.procedure; diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..dffa2ae --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,215 @@ +import { join } from 'path'; + +import { app, shell, session, BrowserWindow, screen } from 'electron'; +import { electronApp, optimizer, is } from '@electron-toolkit/utils'; +import { createIPCHandler } from 'electron-trpc/main'; +import Logger from 'electron-log/main'; + +import icon from '~build/icon.png?asset'; +import { PreferencesSchema } from '~common/schemas'; + +import { appRouter } from './api/root'; +import Preferences from './modules/preferences'; +import Updater from './modules/updater'; +import Addons from './modules/addons'; +import Mods from './modules/mods'; +import { initSelfUpdater } from './modules/selfUpdater'; +import { + detectHardware, + recommendFarClip, + HARDWARE_SCHEMA_VERSION +} from './modules/hardware'; + +Logger.initialize(); +Logger.errorHandler.startCatching(); +Logger.transports.ipc.level = false; +Logger.info('Launcher starting...'); + +app.disableHardwareAcceleration(); + +export let mainWindow: BrowserWindow | null = null; + +const isOnScreen = ( + pos?: { + x: number; + y: number; + width: number; + height: number; + } | null +) => { + if (!pos) return false; + return screen.getAllDisplays().some(d => { + const a = d.workArea; + return ( + pos.x < a.x + a.width && + pos.x + pos.width > a.x && + pos.y < a.y + a.height && + pos.y + pos.height > a.y + ); + }); +}; + +const createWindow = async () => { + const saved = + Preferences.data.rememberPosition && + isOnScreen(Preferences.data.windowPosition) + ? Preferences.data.windowPosition + : undefined; + const position = saved ?? { width: 1000, height: 700 }; + + mainWindow = new BrowserWindow({ + ...position, + minWidth: 1000, + minHeight: 700, + icon, + frame: false, + maximizable: false, + fullscreenable: false, + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + contextIsolation: true, + sandbox: false, + devTools: true + } + }); + + mainWindow.webContents.on('render-process-gone', (_e, details) => { + Logger.error('Renderer process gone:', details); + }); + mainWindow.webContents.on('unresponsive', () => { + Logger.error('Renderer unresponsive'); + }); + + mainWindow.webContents.on( + 'console-message', + (_e, level, message, line, sourceId) => { + const lvl = level === 3 ? 'error' : level === 2 ? 'warn' : 'info'; + Logger[lvl](`[renderer:${lvl}] ${message} (${sourceId}:${line})`); + } + ); + + mainWindow.webContents.on('before-input-event', (_e, input) => { + if (input.type !== 'keyDown') return; + if (input.key === 'F12') { + mainWindow?.webContents.toggleDevTools(); + return; + } + if ((input.control || input.meta) && input.key.toLowerCase() === 'c') + mainWindow?.webContents.copy(); + }); + + createIPCHandler({ router: appRouter, windows: [mainWindow] }); + + mainWindow.on('ready-to-show', () => { + mainWindow?.show(); + }); + mainWindow.webContents.setWindowOpenHandler(details => { + shell.openExternal(details.url); + return { action: 'deny' }; + }); + mainWindow.on('close', () => { + if (!mainWindow) return; + const [x = 0, y = 0] = mainWindow.getPosition(); + const [width = 0, height = 0] = mainWindow.getSize(); + Preferences.data = { windowPosition: { x, y, width, height } }; + }); + + if (is.dev && process.env.ELECTRON_RENDERER_URL) { + mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL); + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')); + } +}; + +const gotSingleInstanceLock = is.dev || app.requestSingleInstanceLock(); + +if (!gotSingleInstanceLock) { + app.quit(); +} else { + app.on('second-instance', () => { + if (!mainWindow) return; + if (mainWindow.isMinimized()) mainWindow.restore(); + if (!mainWindow.isVisible()) mainWindow.show(); + mainWindow.focus(); + }); + + app.whenReady().then(async () => { + // defaults on failure so createWindow() below still runs + try { + Preferences.data = await Preferences.load(); + } catch (e) { + Logger.error('Preferences.load() failed; starting on defaults', e); + Preferences.data = PreferencesSchema.parse({}); + } + + Addons.verify(); + Updater.verify(); + Mods.verify(); + initSelfUpdater(); + + void (async () => { + try { + let hardware = Preferences.data.hardware; + if (!hardware || hardware.schemaVersion < HARDWARE_SCHEMA_VERSION) { + hardware = await detectHardware(); + Preferences.data = { hardware }; + } + const rec = recommendFarClip(hardware ?? null); + if ( + Preferences.data.farClipUserSet !== true && + Preferences.data.config.farClip !== rec + ) + Preferences.data = { + config: { ...Preferences.data.config, farClip: rec } + }; + } catch (e) { + Logger.error('Hardware detection / farClip recommendation failed', e); + } + })(); + + electronApp.setAppUserModelId('st.octowow.launcher'); + + if (app.isPackaged) { + const serverOrigin = new URL( + import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st' + ).origin; + session.defaultSession.webRequest.onHeadersReceived((details, cb) => { + cb({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': [ + [ + "default-src 'self'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + `img-src 'self' data: https://octowow.st https://forum.octowow.st ${serverOrigin}`, + "font-src 'self' data:", + "connect-src 'self'" + ].join('; ') + ] + } + }); + }); + } + + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window); + }); + + await createWindow(); + }); + + let settingsFlushed = false; + app.on('before-quit', event => { + if (settingsFlushed) return; + settingsFlushed = true; + event.preventDefault(); + Promise.race([Preferences.save(), new Promise(r => setTimeout(r, 3000))]) + .catch(e => Logger.error('Failed to flush settings before quit', e)) + .finally(() => app.quit()); + }); + + app.on('window-all-closed', () => { + app.quit(); + }); +} diff --git a/src/main/modules/addons.ts b/src/main/modules/addons.ts new file mode 100644 index 0000000..266121c --- /dev/null +++ b/src/main/modules/addons.ts @@ -0,0 +1,450 @@ +import path from 'node:path'; + +import git, { type ProgressCallback } from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import fs from 'fs-extra'; +import fetch from 'node-fetch'; +import Logger from 'electron-log/main'; + +import { isNotUndef } from '~common/utils'; +import { type AddonData, type TocData } from '~common/schemas'; +import { runWorker } from '~main/utils'; +import gitPull from '~main/workers/gitPull?nodeWorker'; +import gitClone from '~main/workers/gitClone?nodeWorker'; + +import Preferences from './preferences'; +import Observable from './observable'; + +export type AddonsStatus = { + state: 'verifying' | 'done'; + addons: { [name: string]: AddonData }; + available: AddonData[]; +}; + +type AddonsList = { + name: string; + owner: string; + branch?: string; + ref?: string; + git: string; + toc?: TocData; + description?: string; + lastUpdated?: string; + stars?: number; + dependencies?: string[]; +}[]; + +const readTocData = (content: string) => + (content.charCodeAt(0) === 0xfeff ? content.slice(1) : content) + .split('\n') + .filter(l => l.startsWith('## ')) + .map(l => l.slice(3)) + .map(l => { + const idx = l.indexOf(':'); + if (idx === -1) return null; + return [l.slice(0, idx).trim(), l.slice(idx + 1).trim()] as const; + }) + .filter((e): e is readonly [string, string] => !!e) + .reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {} as TocData); + +const isUnsafeFolder = (name?: string) => + !name || name === '.' || name === '..' || /[/\\]/.test(name); + +const ALLOWED_GIT_HOSTS = [ + 'github.com', + 'gitlab.com', + 'gitea.com', + 'codeberg.org', + 'octowow.st' +]; + +const isAllowedGitUrl = (url: string) => { + try { + const parsed = new URL(url); + if (parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return ALLOWED_GIT_HOSTS.some(h => host === h || host.endsWith('.' + h)); + } catch { + return false; + } +}; + +const fetchAddons = async () => { + try { + const response = await fetch( + `${ + import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st' + }/api/addons.json` + ); + return (await response.json()) as AddonsList; + } catch (e) { + Logger.error('Failed to reach update server', e); + return []; + } +}; + +class AddonsClass extends Observable { + protected _value: AddonsStatus = { + state: 'done', + addons: {}, + available: [] + }; + + get status() { + return this._value; + } + private set status(v: AddonsStatus) { + this._value = v; + this._notifyObservers(v); + } + + #onProgress = + (folder: string, data: AddonData): ProgressCallback => + progress => { + const getPhase = (step: string) => { + switch (step) { + case 'Counting objects': + return 1; + case 'Compressing objects': + return 2; + case 'Receiving objects': + return 3; + case 'Resolving deltas': + return 4; + case 'Analyzing workdir': + return 5; + case 'Updating workdir': + return 6; + default: + return 0; + } + }; + this.#setAddon(folder, { + ...data, + progress: `${Math.round( + (progress.loaded / (progress.total ?? progress.loaded)) * 100 + )}% (${getPhase(progress.phase)}/6)` + }); + }; + + async checkGitUrl(url: string) { + const clean = url.trim().replace(/\/+$/, ''); + const gitUrl = clean.endsWith('.git') ? clean : `${clean}.git`; + if (!isAllowedGitUrl(gitUrl)) return undefined; + try { + await git.getRemoteInfo({ + http, + url: gitUrl + }); + + let preview: string | undefined; + try { + if (isAllowedGitUrl(url)) { + const response = await fetch(url).then(r => r.text()); + preview = response.match( + /property="og:image" content="([^"]*)"/ + )?.[1]; + } + } catch { + } + + const folder = gitUrl.slice(0, -4).split('/').at(-1); + if (isUnsafeFolder(folder)) return undefined; + return { + status: 'available', + folder, + git: gitUrl, + preview + } as AddonData; + } catch { + return undefined; + } + } + + async verify() { + if (this.status.state !== 'done') return; + + this.status = { + ...this.status, + state: 'verifying' + }; + + const remoteAddons = await fetchAddons(); + const available: AddonData[] = remoteAddons.map(a => ({ + status: 'available', + git: a.git, + toc: a.toc, + description: a.description, + folder: a.name, + branch: a.branch, + ref: a.ref + })); + + const clientPath = Preferences.data.clientDir; + if (!clientPath) { + this.status = { state: 'done', addons: {}, available }; + return; + } + + const addonsPath = path.join(clientPath, 'Interface', 'Addons'); + const dirs = (await fs.pathExists(addonsPath)) + ? await fs.readdir(addonsPath) + : []; + const addons: AddonsStatus['addons'] = Object.fromEntries( + dirs + .filter(d => !d.startsWith('Blizzard_') && !/\.(tmp|bak)$/.test(d)) + .map(name => [name, { status: 'fetching' as const, folder: name }]) + ); + + this.status = { state: 'verifying', addons, available }; + + const verifyOne = async (folder: string) => { + const dir = path.join(addonsPath, folder); + + if (!fs.existsSync(path.join(dir, `${folder}.toc`))) { + this.#setAddon(folder, { + status: 'invalid', + error: 'Missing .toc file', + folder + }); + return; + } + + const toc = await readTocData( + await fs.readFile(path.join(dir, `${folder}.toc`), 'utf-8') + ); + + const remote = await git + .listRemotes({ fs, dir }) + .then(r => r[0]) + .catch(() => null); + + const avail = remoteAddons.find(a => a.name === folder); + if (!remote) { + Logger.log(`Addon "${folder}" is not a git repository`); + this.#setAddon( + folder, + avail + ? { + status: 'outOfDate', + git: avail.git, + toc, + description: avail.description, + folder + } + : { status: 'unknown', toc, folder } + ); + return; + } + + try { + await git.fetch({ fs, dir, http, tags: true }); + + const branch = await git.currentBranch({ fs, dir }); + + const localCommit = await git + .log({ fs, dir, ref: 'HEAD', depth: 1 }) + .then(r => r[0].oid) + .catch(() => null); + + const remoteCommit = avail?.ref + ? await git.resolveRef({ fs, dir, ref: avail.ref }).catch(() => null) + : await git + .log({ fs, dir, ref: `${remote.remote}/${branch}`, depth: 1 }) + .then(r => r[0].oid) + .catch(() => null); + + const status = await git.statusMatrix({ fs, dir }); + const hasChanges = status.some( + ([_, HEAD, index, workdir]) => HEAD !== index || index !== workdir + ); + + const isUpToDate = + !hasChanges && remoteCommit && localCommit === remoteCommit; + this.#setAddon(folder, { + git: remote.url, + status: isUpToDate ? 'upToDate' : 'outOfDate', + toc, + description: avail?.description, + ref: avail?.ref, + folder + }); + + Logger.log( + isUpToDate + ? `Addon "${folder}" is up to date${ + avail?.ref ? ` (pinned ${avail.ref})` : '' + }` + : `Addon "${folder}" has an update available` + ); + } catch (e) { + this.#setAddon(folder, { + git: remote.url, + status: 'invalid', + error: 'Failed to verify', + toc, + folder + }); + Logger.error(`Addon "${folder}" failed to verify`, e); + } + }; + + const folders = Object.keys(addons); + const VERIFY_CONCURRENCY = 6; + let idx = 0; + await Promise.all( + Array.from( + { length: Math.min(VERIFY_CONCURRENCY, folders.length) }, + async () => { + while (true) { + const i = idx++; + if (i >= folders.length) return; + await verifyOne(folders[i]); + } + } + ) + ); + + this.status = { ...this.status, state: 'done' }; + } + + async update( + toUpdate = Object.values(this.status.addons) + .filter(e => e.status === 'outOfDate') + .map(e => e.folder) + .filter(isNotUndef) + ) { + const clientPath = Preferences.data.clientDir; + if (!clientPath) return; + if (this.status.state !== 'done') return; + + const addonsPath = path.join(clientPath, 'Interface', 'Addons'); + + for (const folder of toUpdate) { + if (this.status.addons[folder]?.status === 'downloading') continue; + const dir = path.join(addonsPath, folder); + + const avail = this.status.available.find(a => a.folder === folder); + const data: AddonData = { + ...avail, + ...this.status.addons[folder], + status: 'downloading' + }; + this.#setAddon(folder, data); + + const remote = await git + .listRemotes({ fs, dir }) + .then(r => r?.[0]) + .catch(() => null); + + try { + if (!remote) { + await runWorker( + gitClone, + { dir, url: data.git, ref: data.ref ?? data.branch }, + { onProgress: this.#onProgress(folder, data) } + ); + } else { + const branch = + (await git.currentBranch({ fs, dir })) ?? avail?.branch ?? 'master'; + await runWorker( + gitPull, + { + dir, + remote: remote.remote, + branch, + ref: avail?.ref + }, + { onProgress: this.#onProgress(folder, data) } + ); + } + const toc = readTocData( + await fs.readFile(path.join(dir, `${folder}.toc`), 'utf-8') + ); + + this.#setAddon(folder, { ...data, toc, status: 'upToDate' }); + Logger.log(`Updated addon "${folder}"`); + } catch (e) { + this.#setAddon(folder, { + ...data, + status: 'invalid', + error: 'Failed to update' + }); + Logger.error(`Addon "${folder}" failed to update`, e); + } + } + } + + async remove(toRemove: string[]) { + const clientPath = Preferences.data.clientDir; + if (!clientPath) return; + if (this.status.state !== 'done') return; + + for (const folder of toRemove) { + const dir = path.join(clientPath, 'Interface', 'Addons', folder); + if (fs.existsSync(dir)) await fs.remove(dir); + this.#setAddon(folder); + Logger.log(`Removed addon "${folder}"`); + } + } + + async install(data: AddonData) { + const clientPath = Preferences.data.clientDir; + if (!clientPath) return; + if (isUnsafeFolder(data.folder)) { + Logger.error(`Refusing addon with unsafe folder name: "${data.folder}"`); + this.#setAddon(data.folder, { + ...data, + status: 'invalid', + error: 'Invalid addon name' + }); + return; + } + + if (!data.git || !isAllowedGitUrl(data.git)) { + Logger.error(`Refusing addon from disallowed git host: "${data.git}"`); + this.#setAddon(data.folder, { + ...data, + status: 'invalid', + error: 'Addon URL is not from an allowed git host' + }); + return; + } + + const addonsPath = path.join(clientPath, 'Interface', 'Addons'); + const dir = path.join(addonsPath, data.folder); + try { + await runWorker( + gitClone, + { dir, url: data.git, ref: data.ref ?? data.branch }, + { onProgress: this.#onProgress(data.folder, data) } + ); + const toc = await readTocData( + await fs.readFile(path.join(dir, `${data.folder}.toc`), 'utf-8') + ); + this.#setAddon(data.folder, { ...data, toc, status: 'upToDate' }); + Logger.log(`Installed addon "${data.folder}"`); + } catch (e) { + this.#setAddon(data.folder, { + ...data, + status: 'invalid', + error: 'Failed to install' + }); + Logger.error(`Addon "${data.folder}" failed to install`, e); + } + } + + #setAddon(folder: string, data?: AddonData) { + const { [folder]: _, ...addons } = this.status.addons; + this.status = { + ...this.status, + addons: data ? { ...addons, [folder]: data } : addons + }; + } +} + +const Addons = new AddonsClass(); +export default Addons; diff --git a/src/main/modules/aria2.ts b/src/main/modules/aria2.ts new file mode 100644 index 0000000..b033a4a --- /dev/null +++ b/src/main/modules/aria2.ts @@ -0,0 +1,476 @@ +import crypto from 'crypto'; +import path from 'path'; +import { spawn, type ChildProcess } from 'child_process'; + +import { app } from 'electron'; +import fs from 'fs-extra'; +import Logger from 'electron-log/main'; + +import { mapPort, type PortMapping } from './upnp'; + +const TORRENT_NAME = 'client'; + +const bin = () => + app.isPackaged + ? path.join(process.resourcesPath, 'aria2c.exe') + : path.join(app.getAppPath(), 'resources', 'aria2c.exe'); + +type SyncOpts = { + torrentUrl: string; + clientDir: string; + totalBytes?: number; + checkIntegrity?: boolean; + seedTime?: number; + selectFiles?: number[]; + onProgress?: (p: SyncProgress) => void; + signal?: AbortSignal; +}; + +export type SyncProgress = { + progress: number; + bytesDone: number; + bytesTotal: number; + bytesPerSecond: number; +}; + +const ensureJunction = async (clientDir: string): Promise => { + await fs.ensureDir(clientDir); + const staging = path.join(app.getPath('userData'), 'torrent-root'); + await fs.ensureDir(staging); + const link = path.join(staging, TORRENT_NAME); + + const target = path.resolve(clientDir); + try { + const cur = await fs.lstat(link); + if (cur.isSymbolicLink() || cur.isDirectory()) { + const resolved = await fs.realpath(link).catch(() => ''); + if (path.resolve(resolved) === target) return staging; + } + await fs.remove(link); + } catch {} + await fs.symlink(target, link, 'junction'); + return staging; +}; + +const SIZE_UNITS: Record = { + B: 1, + KiB: 1024, + MiB: 1024 ** 2, + GiB: 1024 ** 3, + TiB: 1024 ** 4 +}; + +const toBytes = (s: string): number => { + const m = /^([\d.]+)(B|KiB|MiB|GiB|TiB)$/.exec(s.trim()); + if (!m) return 0; + return parseFloat(m[1]) * (SIZE_UNITS[m[2]] ?? 1); +}; + +const parseProgress = ( + line: string, + totalHint: number +): SyncProgress | undefined => { + const frac = + /([\d.]+(?:B|KiB|MiB|GiB|TiB))\/([\d.]+(?:B|KiB|MiB|GiB|TiB))\((\d+)%\)/.exec( + line + ); + if (!frac) return undefined; + const dl = /DL:([\d.]+(?:B|KiB|MiB|GiB|TiB))/.exec(line); + return { + progress: parseInt(frac[3], 10) / 100, + bytesDone: toBytes(frac[1]), + bytesTotal: toBytes(frac[2]) || totalHint, + bytesPerSecond: dl ? toBytes(dl[1]) : 0 + }; +}; + +export const syncClient = (opts: SyncOpts): Promise => + new Promise((resolve, reject) => { + let child: ChildProcess | undefined; + ensureJunction(opts.clientDir) + .then(dir => { + const args = [ + `--dir=${dir}`, + `--seed-time=${opts.seedTime ?? 0}`, + `--check-integrity=${opts.checkIntegrity ? 'true' : 'false'}`, + '--bt-save-metadata=true', + '--bt-remove-unselected-file=false', + '--continue=true', + '--allow-overwrite=true', + '--auto-file-renaming=false', + '--file-allocation=none', + '--disk-cache=128M', + '--stream-piece-selector=inorder', + '--max-tries=0', + '--retry-wait=5', + '--bt-stop-timeout=120', + '--auto-save-interval=15', + '--summary-interval=1', + '--console-log-level=warn', + '--enable-dht=true', + '--bt-enable-lpd=true', + '--max-connection-per-server=8', + '--split=16', + '--min-split-size=1M', + ...(opts.selectFiles?.length + ? [`--select-file=${opts.selectFiles.join(',')}`] + : []), + '--stop-with-process=' + process.pid, + opts.torrentUrl + ]; + Logger.log(`aria2c ${args.join(' ')}`); + child = spawn(bin(), args, { windowsHide: true }); + + const onLine = (buf: Buffer) => { + for (const line of buf.toString().split(/\r?\n/)) { + if (!line.trim()) continue; + const p = parseProgress(line, opts.totalBytes ?? 0); + if (p) opts.onProgress?.(p); + else Logger.log(`[aria2] ${line}`); + } + }; + child.stdout?.on('data', onLine); + child.stderr?.on('data', onLine); + + opts.signal?.addEventListener('abort', () => child?.kill()); + + child.on('error', reject); + child.on('close', code => { + if (code === 0) resolve(); + else reject(new Error(`aria2c exited with code ${code}`)); + }); + }) + .catch(reject); + }); + +export const aria2Available = () => fs.pathExists(bin()); + +export const downloadIsComplete = async (): Promise => { + const control = path.join( + app.getPath('userData'), + 'torrent-root', + `${TORRENT_NAME}.aria2` + ); + return !(await fs.pathExists(control)); +}; + +export const clearTorrentResumeState = async (): Promise => { + const dir = path.join(app.getPath('userData'), 'torrent-root'); + await Promise.all([ + fs.remove(path.join(dir, `${TORRENT_NAME}.aria2`)), + fs.remove(path.join(dir, `${TORRENT_NAME}.torrent`)) + ]); +}; + +export const torrentUrl = (): string | undefined => + import.meta.env.MAIN_VITE_CLIENT_TORRENT_URL || undefined; + +export const isTorrentMode = (): boolean => !!torrentUrl(); + +export const raidVisualsUrl = (): string | undefined => + import.meta.env.MAIN_VITE_RAID_VISUALS_URL || undefined; + +export const clientPatchUrl = (): string | undefined => + import.meta.env.MAIN_VITE_CLIENT_PATCH_URL || undefined; + +export const fetchTorrentSha = async (url: string): Promise => { + const r = await fetch(url); + if (!r.ok) throw new Error(`HTTP ${r.status}`); + const buf = Buffer.from(await r.arrayBuffer()); + return crypto.createHash('sha1').update(buf).digest('hex'); +}; + +const bdecode = (buf: Buffer, pos = 0): [unknown, number] => { + if (pos >= buf.length) throw new Error('bencode: unexpected end of data'); + const ch = buf[pos]; + if (ch === 0x69) { + const end = buf.indexOf(0x65, pos); + if (end === -1) throw new Error('bencode: unterminated integer'); + return [parseInt(buf.toString('latin1', pos + 1, end), 10), end + 1]; + } + if (ch === 0x6c) { + const list: unknown[] = []; + let p = pos + 1; + while (buf[p] !== 0x65) { + if (p >= buf.length) throw new Error('bencode: unterminated list'); + const [v, np] = bdecode(buf, p); + list.push(v); + p = np; + } + return [list, p + 1]; + } + if (ch === 0x64) { + const dict: Record = {}; + let p = pos + 1; + while (buf[p] !== 0x65) { + if (p >= buf.length) throw new Error('bencode: unterminated dict'); + const [k, kp] = bdecode(buf, p); + const [v, vp] = bdecode(buf, kp); + dict[k as string] = v; + p = vp; + } + return [dict, p + 1]; + } + const colon = buf.indexOf(0x3a, pos); + if (colon === -1) throw new Error('bencode: unterminated string length'); + const len = parseInt(buf.toString('latin1', pos, colon), 10); + if (!Number.isInteger(len) || len < 0 || colon + 1 + len > buf.length) + throw new Error('bencode: invalid string length'); + const start = colon + 1; + return [buf.toString('latin1', start, start + len), start + len]; +}; + +const torrentDataArchives = (torrentBytes: Buffer): Set => { + const [torrent] = bdecode(torrentBytes) as [ + { info?: { files?: { path?: string[] }[] } }, + number + ]; + const files = torrent?.info?.files ?? []; + return new Set( + files + .filter( + f => + f.path?.length === 2 && + f.path[0] === 'Data' && + /\.mpq$/i.test(f.path[1]) + ) + .map(f => f.path![1].toLowerCase()) + ); +}; + +const LOCALE_DIRS = new Set([ + 'enus', + 'engb', + 'encn', + 'entw', + 'kokr', + 'frfr', + 'dede', + 'zhcn', + 'zhtw', + 'eses', + 'esmx', + 'ruru', + 'ptbr', + 'ptpt', + 'itit' +]); + +const torrentDataDirs = (torrentBytes: Buffer): Set => { + const [torrent] = bdecode(torrentBytes) as [ + { info?: { files?: { path?: string[] }[] } }, + number + ]; + const files = torrent?.info?.files ?? []; + return new Set( + files + .filter(f => (f.path?.length ?? 0) >= 3 && f.path![0] === 'Data') + .map(f => f.path![1].toLowerCase()) + ); +}; + +export const pruneStaleArchives = async ( + clientDir: string, + url: string, + _owned: Set +): Promise => { + try { + const r = await fetch(url); + if (!r.ok) return []; + const bytes = Buffer.from(await r.arrayBuffer()); + const expected = torrentDataArchives(bytes); + if (!expected.size) return []; + for (const u of [clientPatchUrl(), raidVisualsUrl()]) + if (u) expected.add(path.basename(u).toLowerCase()); + const usedDirs = torrentDataDirs(bytes); + const dataDir = path.join(clientDir, 'Data'); + const onDisk = await fs.readdir(dataDir).catch(() => []); + const removed: string[] = []; + for (const name of onDisk) { + const lc = name.toLowerCase(); + const full = path.join(dataDir, name); + const st = await fs.stat(full).catch(() => null); + if (!st) continue; + if (st.isDirectory()) { + if (LOCALE_DIRS.has(lc) && !usedDirs.has(lc)) { + await fs.remove(full); + removed.push(name + '/'); + } + continue; + } + if (!/\.mpq$/i.test(name) || expected.has(lc)) continue; + await fs.remove(full); + removed.push(name); + } + return removed; + } catch (e) { + Logger.warn('Prune of stale archives failed', e); + return []; + } +}; + +export const torrentDownloadSelection = async ( + clientDir: string, + url: string, + dropMismatched = false +): Promise => { + try { + const r = await fetch(url); + if (!r.ok) return null; + const [torrent] = bdecode(Buffer.from(await r.arrayBuffer())) as [ + { info?: { files?: { path?: string[]; length?: number }[] } }, + number + ]; + const files = torrent?.info?.files ?? []; + if (!files.length) return null; + const need: number[] = []; + for (let i = 0; i < files.length; i++) { + const f = files[i]; + if (!f.path?.length || typeof f.length !== 'number') return null; + const dest = path.join(clientDir, ...f.path); + const st = await fs.stat(dest).catch(() => null); + if (!st) { + need.push(i + 1); + continue; + } + if (st.size !== f.length) { + if (dropMismatched || st.size > f.length) + await fs.remove(dest).catch(() => {}); + need.push(i + 1); + } + } + return need; + } catch (e) { + Logger.warn('Torrent selection computation failed', e); + return null; + } +}; + +export const torrentTreeIntact = async ( + clientDir: string, + url: string +): Promise => { + try { + const r = await fetch(url); + if (!r.ok) return false; + const [torrent] = bdecode(Buffer.from(await r.arrayBuffer())) as [ + { info?: { files?: { path?: string[]; length?: number }[] } }, + number + ]; + const files = torrent?.info?.files ?? []; + if (!files.length) return false; + for (const f of files) { + if (!f.path?.length || typeof f.length !== 'number') return false; + const st = await fs + .stat(path.join(clientDir, ...f.path)) + .catch(() => null); + if (!st || st.size !== f.length) return false; + } + return true; + } catch (e) { + Logger.warn('Torrent tree check failed', e); + return false; + } +}; + +const LOCALE_ASSERT_OFFSET = 0x1b2115; +const pristineWowPath = () => + path.join(app.getPath('userData'), 'base-WoW.exe'); + +export const refreshPristineWow = async (clientDir: string): Promise => { + const exe = path.join(clientDir, 'WoW.exe'); + if (!(await fs.pathExists(exe))) return; + const fd = await fs.open(exe, 'r'); + try { + const b = Buffer.alloc(1); + await fs.read(fd, b, 0, 1, LOCALE_ASSERT_OFFSET); + if (b[0] === 0xa1) { + await fs.copy(exe, pristineWowPath(), { overwrite: true }); + Logger.log('Cached pristine WoW.exe base'); + } + } finally { + await fs.close(fd); + } +}; + +export const readPristineWow = async (clientDir: string): Promise => { + const cache = pristineWowPath(); + if (await fs.pathExists(cache)) return fs.readFile(cache); + return fs.readFile(path.join(clientDir, 'WoW.exe')); +}; + +let seeder: ChildProcess | undefined; +let mapping: Promise | undefined; +let wantSeeding = false; +let starting = false; + +const SEED_PORT = 6881; +const SEED_TIME_MINUTES = 525600; + +export const isSeeding = (): boolean => !!seeder; + +const releaseMapping = (): void => { + const m = mapping; + mapping = undefined; + if (m) void m.then(x => x.stop()).catch(() => {}); +}; + +export const stopSeeding = (): void => { + wantSeeding = false; + seeder?.kill(); + seeder = undefined; + releaseMapping(); +}; + +export const startSeeding = async ( + clientDir: string, + uploadLimit = '2M' +): Promise => { + wantSeeding = true; + if (seeder || starting) return; + starting = true; + try { + const url = torrentUrl(); + if (!url) return; + const dir = await ensureJunction(clientDir); + if (!wantSeeding || seeder) return; + const args = [ + `--dir=${dir}`, + '--bt-seed-unverified=true', + `--seed-time=${SEED_TIME_MINUTES}`, + '--check-integrity=false', + '--continue=true', + '--bt-save-metadata=true', + '--enable-dht=true', + '--bt-enable-lpd=true', + `--listen-port=${SEED_PORT}`, + `--dht-listen-port=${SEED_PORT}`, + `--max-overall-upload-limit=${uploadLimit}`, + '--summary-interval=0', + '--console-log-level=warn', + '--stop-with-process=' + process.pid, + url + ]; + Logger.log('aria2c (seed) ' + args.join(' ')); + const child = spawn(bin(), args, { windowsHide: true }); + seeder = child; + const onExit = () => { + if (seeder === child) { + seeder = undefined; + releaseMapping(); + } + }; + child.on('close', onExit); + child.on('error', e => { + Logger.warn('Seeder failed', e); + onExit(); + }); + + mapping = mapPort(SEED_PORT, { description: 'OctoWoW' }); + void mapping.catch(() => undefined); + } catch (e) { + Logger.warn('startSeeding failed', e); + } finally { + starting = false; + } +}; diff --git a/src/main/modules/defender.ts b/src/main/modules/defender.ts new file mode 100644 index 0000000..0b21c2d --- /dev/null +++ b/src/main/modules/defender.ts @@ -0,0 +1,192 @@ +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { app } from 'electron'; +import Logger from 'electron-log/main'; + +import Preferences from './preferences'; + +export type ExclusionResult = { ok: boolean; error?: string; paths?: string[] }; + +const psSingleQuote = (s: string) => `'${s.replace(/'/g, "''")}'`; + +export const addDefenderExclusions = async (): Promise => { + if (os.platform() !== 'win32') + return { + ok: false, + error: 'Antivirus exclusions are only needed on Windows.' + }; + + const clientDir = Preferences.data.clientDir; + if (!clientDir) + return { + ok: false, + error: 'Set your game folder first, then add the exclusion.' + }; + + const launcherDir = + process.env.PORTABLE_EXECUTABLE_DIR ?? path.dirname(app.getPath('exe')); + const paths = [...new Set([clientDir, launcherDir])]; + + const resultFile = path.join( + os.tmpdir(), + `octo-defender-${process.pid}-${Date.now()}.txt` + ); + const write = (v: string) => + `Set-Content -LiteralPath ${psSingleQuote( + resultFile + )} -Value "${v}" -Encoding ASCII`; + + const inner = [ + 'try {', + ...paths.map( + p => + ` Add-MpPreference -ExclusionPath ${psSingleQuote( + p + )} -ErrorAction Stop` + ), + ' Add-MpPreference -ExclusionProcess "WoW.exe" -ErrorAction Stop', + ' Add-MpPreference -ExclusionProcess "VanillaFixes.exe" -ErrorAction Stop', + ` ${write('OK')}`, + '} catch {', + ' $t = $false', + ' try { $t = (Get-MpComputerStatus).IsTamperProtected } catch {}', + ` if ($t) { ${write('TAMPER')} } else { ${write('FAIL')} }`, + '}' + ].join('\n'); + const encoded = Buffer.from(inner, 'utf16le').toString('base64'); + + const outer = + 'try { Start-Process powershell -Verb RunAs -WindowStyle Hidden -Wait ' + + "-ArgumentList '-NoProfile','-NonInteractive'," + + `'-EncodedCommand','${encoded}' } catch { exit 1 }`; + + return new Promise(resolve => { + const child = spawn( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-Command', outer], + { windowsHide: true } + ); + let stderr = ''; + child.stderr.on('data', d => (stderr += String(d))); + child.on('error', e => { + Logger.error('Failed to launch PowerShell for Defender exclusion', e); + resolve({ ok: false, error: 'Could not run Windows PowerShell.' }); + }); + child.on('exit', code => { + let result: string | null = null; + try { + result = fs.readFileSync(resultFile, 'utf8').trim(); + } catch {} + try { + fs.rmSync(resultFile, { force: true }); + } catch {} + + if (result === 'OK') { + Logger.info(`Added Defender exclusions: ${paths.join(', ')}`); + resolve({ ok: true, paths }); + return; + } + if (result === 'TAMPER') { + Logger.error('Defender exclusion blocked by Tamper Protection'); + resolve({ + ok: false, + error: + 'Windows Security Tamper Protection is blocking this. Turn it off in Windows Security, or add your game folder by hand under Exclusions.' + }); + return; + } + if (result === 'FAIL') { + Logger.error(`Defender exclusion failed: ${stderr}`.trim()); + resolve({ + ok: false, + error: + 'Windows would not add the exclusion. You can add your game folder by hand in Windows Security, under Exclusions.' + }); + return; + } + Logger.warn( + `Defender exclusion: no result (exit ${code}) ${stderr}`.trim() + ); + resolve({ + ok: false, + error: + 'Windows did not grant permission. Click Yes on the User Account Control prompt to add the exclusion.' + }); + }); + }); +}; + +const SENSITIVE_FILES = [ + 'WoW.exe', + 'VanillaFixes.exe', + 'd3d9.dll', + 'UnitXP_SP3.dll', + 'nampower.dll', + 'VfPatcher.dll', + 'VanillaHelpers.dll', + 'VanillaMultiMonitorFix.dll', + 'transmogfix.dll' +]; + +export const detectAntivirusBlocks = async (): Promise => { + if (os.platform() !== 'win32') return []; + + const clientDir = Preferences.data.clientDir; + const launcherDir = + process.env.PORTABLE_EXECUTABLE_DIR ?? path.dirname(app.getPath('exe')); + const roots = [clientDir, launcherDir] + .filter((p): p is string => !!p) + .map(p => p.toLowerCase()); + if (!roots.length) return []; + + const blocked = new Set(); + + if (clientDir && Preferences.data.syncedTorrentHash) + for (const name of SENSITIVE_FILES) + if (!fs.existsSync(path.join(clientDir, name))) blocked.add(name); + + const script = + 'Get-MpThreatDetection | Where-Object ' + + '{ $_.InitialDetectionTime -gt (Get-Date).AddHours(-12) } | ' + + 'Select-Object -ExpandProperty Resources'; + await new Promise(resolve => { + const child = spawn( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-Command', script], + { windowsHide: true } + ); + let out = ''; + let settled = false; + const timer = setTimeout(() => { + try { + child.kill(); + } catch {} + }, 15_000); + const finish = () => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve(); + }; + child.stdout.on('data', d => (out += String(d))); + child.on('error', finish); + child.on('exit', () => { + for (const line of out.split(/\r?\n/)) { + const m = /^file:_?(.+)$/.exec(line.trim()); + if (!m) continue; + const full = m[1]; + if ( + roots.some(r => full.toLowerCase().startsWith(r)) && + !fs.existsSync(full) + ) + blocked.add(path.basename(full)); + } + finish(); + }); + }); + + return [...blocked]; +}; diff --git a/src/main/modules/displays.ts b/src/main/modules/displays.ts new file mode 100644 index 0000000..b43f074 --- /dev/null +++ b/src/main/modules/displays.ts @@ -0,0 +1,146 @@ +import os from 'node:os'; +import { spawn } from 'node:child_process'; + +import Logger from 'electron-log/main'; + +export type DisplayDevice = { + index: number; + deviceName: string; + deviceString: string; + attached: boolean; + primary: boolean; + width: number; + height: number; + refresh: number; + modes: string[]; +}; + +const SCRIPT = [ + '$ErrorActionPreference = "Stop"', + "$ProgressPreference = 'SilentlyContinue'", + "Add-Type -TypeDefinition @'", + 'using System;', + 'using System.Runtime.InteropServices;', + 'public static class VmmfDisplays {', + ' [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]', + ' public struct DISPLAY_DEVICE {', + ' public int cb;', + ' [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string DeviceName;', + ' [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceString;', + ' public int StateFlags;', + ' [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceID;', + ' [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceKey;', + ' }', + ' [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]', + ' public struct DEVMODE {', + ' [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string dmDeviceName;', + ' public short dmSpecVersion; public short dmDriverVersion; public short dmSize; public short dmDriverExtra;', + ' public int dmFields; public int dmPositionX; public int dmPositionY;', + ' public int dmDisplayOrientation; public int dmDisplayFixedOutput;', + ' public short dmColor; public short dmDuplex; public short dmYResolution; public short dmTTOption; public short dmCollate;', + ' [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string dmFormName;', + ' public short dmLogPixels; public int dmBitsPerPel; public int dmPelsWidth; public int dmPelsHeight;', + ' public int dmDisplayFlags; public int dmDisplayFrequency;', + ' public int dmICMMethod; public int dmICMIntent; public int dmMediaType; public int dmDitherType;', + ' public int dmReserved1; public int dmReserved2; public int dmPanningWidth; public int dmPanningHeight;', + ' }', + ' [DllImport("user32.dll", EntryPoint="EnumDisplayDevicesA", CharSet=CharSet.Ansi)]', + ' public static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags);', + ' [DllImport("user32.dll", EntryPoint="EnumDisplaySettingsA", CharSet=CharSet.Ansi)]', + ' public static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);', + '}', + "'@", + 'for ($i = 0; ; $i++) {', + ' $dd = New-Object VmmfDisplays+DISPLAY_DEVICE', + ' $dd.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($dd)', + ' if (-not [VmmfDisplays]::EnumDisplayDevices([NullString]::Value, $i, [ref]$dd, 0)) { break }', + ' $dm = New-Object VmmfDisplays+DEVMODE', + ' $dm.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($dm)', + ' $cur = ""', + ' if ([VmmfDisplays]::EnumDisplaySettings($dd.DeviceName, -1, [ref]$dm)) {', + ' $cur = "$($dm.dmPelsWidth)|$($dm.dmPelsHeight)|$($dm.dmDisplayFrequency)"', + ' }', + ' $modes = New-Object System.Collections.Generic.HashSet[string]', + ' for ($m = 0; ; $m++) {', + ' $d2 = New-Object VmmfDisplays+DEVMODE', + ' $d2.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($d2)', + ' if (-not [VmmfDisplays]::EnumDisplaySettings($dd.DeviceName, $m, [ref]$d2)) { break }', + ' [void]$modes.Add("$($d2.dmPelsWidth)x$($d2.dmPelsHeight)")', + ' }', + ' Write-Output ("{0}`t{1}`t{2}`t{3}`t{4}`t{5}" -f $i, $dd.DeviceName, $dd.DeviceString, $dd.StateFlags, $cur, ($modes -join ","))', + '}' +].join('\n'); + +const parseRow = (line: string): DisplayDevice | undefined => { + const f = line.split('\t'); + if (f.length < 6) return undefined; + const index = Number(f[0]); + const stateFlags = Number(f[3]); + if (!Number.isInteger(index) || index < 0 || !Number.isInteger(stateFlags)) + return undefined; + const [w, h, hz] = (f[4] || '').split('|').map(Number); + return { + index, + deviceName: f[1], + deviceString: f[2], + attached: (stateFlags & 1) !== 0, + primary: (stateFlags & 4) !== 0, + width: Number.isFinite(w) ? w : 0, + height: Number.isFinite(h) ? h : 0, + refresh: Number.isFinite(hz) ? hz : 0, + modes: (f[5] || '').split(',').filter(Boolean) + }; +}; + +export const enumerateDisplays = (): Promise => { + if (os.platform() !== 'win32') return Promise.resolve(null); + + const encoded = Buffer.from(SCRIPT, 'utf16le').toString('base64'); + + return new Promise(resolve => { + let settled = false; + const finish = (v: DisplayDevice[] | null) => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve(v); + }; + + const child = spawn( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], + { windowsHide: true } + ); + + const timer = setTimeout(() => { + child.kill(); + Logger.warn('Display enumeration timed out'); + finish(null); + }, 10000); + + let stdout = ''; + child.stdout.on('data', d => (stdout += String(d))); + child.on('error', e => { + Logger.warn('Display enumeration failed to launch PowerShell', e); + finish(null); + }); + child.on('exit', code => { + const devices = stdout + .split(/\r?\n/) + .map(parseRow) + .filter((d): d is DisplayDevice => d !== undefined); + if (code === 0 && devices.length) { + Logger.info(`Enumerated ${devices.length} display device(s)`); + finish(devices); + } else { + Logger.warn('Display enumeration returned nothing usable'); + finish(null); + } + }); + }); +}; + +export const detectPrimaryDisplayIndex = async (): Promise => { + const devices = await enumerateDisplays(); + return devices?.find(d => d.primary && d.attached)?.index ?? null; +}; diff --git a/src/main/modules/dllsTxt.ts b/src/main/modules/dllsTxt.ts new file mode 100644 index 0000000..2770efc --- /dev/null +++ b/src/main/modules/dllsTxt.ts @@ -0,0 +1,79 @@ +import path from 'path'; + +import fs from 'fs-extra'; + +let queue: Promise = Promise.resolve(); + +const serial = (fn: () => Promise): Promise => { + const next = queue.then(fn, fn); + queue = next.catch(() => {}); + return next; +}; + +const dllsPath = (clientDir: string) => path.join(clientDir, 'dlls.txt'); + +const readLines = async (clientDir: string): Promise => { + const file = dllsPath(clientDir); + if (!(await fs.pathExists(file))) return []; + const text = await fs.readFile(file, 'utf8'); + return text.split(/\r?\n/); +}; + +const dllNames = (lines: string[]) => + lines.map(l => l.trim()).filter(l => l && !l.startsWith('#')); + +// keep VanillaFixes' consent cache in step with dlls.txt so it won't re-prompt +const writeCache = async (clientDir: string, names: string[]) => { + const cache = path.join(clientDir, 'dlls.txt.cache'); + if (!names.length) { + await fs.remove(cache).catch(() => {}); + return; + } + const body = names.map(n => path.win32.join(clientDir, n)).join('\r\n'); + await fs.writeFile(cache, body, 'utf8').catch(() => {}); +}; + +const writeLines = async (clientDir: string, lines: string[]) => { + const file = dllsPath(clientDir); + const trimmed = lines.join('\n').replace(/\n+$/, ''); + if (!trimmed.trim()) { + if (await fs.pathExists(file)) await fs.remove(file); + await writeCache(clientDir, []); + return; + } + await fs.writeFile(file, trimmed + '\n', 'utf8'); + await writeCache(clientDir, dllNames(lines)); +}; + +export const syncVanillaFixesCache = (clientDir: string) => + serial(async () => + writeCache(clientDir, dllNames(await readLines(clientDir))) + ); + +const matches = (line: string, name: string) => + line.trim().toLowerCase() === name.toLowerCase(); + +export const addDll = (clientDir: string, name: string) => + serial(async () => { + const lines = await readLines(clientDir); + if (lines.some(l => matches(l, name))) return; + lines.push(name); + await writeLines(clientDir, lines); + }); + +export const removeDll = (clientDir: string, name: string) => + serial(async () => { + const lines = await readLines(clientDir); + const next = lines.filter(l => !matches(l, name)); + if (next.length === lines.length) return; + await writeLines(clientDir, next); + }); + +export const hasDll = (clientDir: string, name: string) => + serial(async () => { + const lines = await readLines(clientDir); + return lines.some(l => matches(l, name)); + }); + +export const listDlls = (clientDir: string): Promise => + serial(async () => dllNames(await readLines(clientDir))); diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts new file mode 100644 index 0000000..4b55b3e --- /dev/null +++ b/src/main/modules/hardware.ts @@ -0,0 +1,132 @@ +import os from 'node:os'; +import { spawn } from 'node:child_process'; + +import { app } from 'electron'; +import Logger from 'electron-log/main'; + +import type { HardwareInfo } from '~common/schemas'; + +export const HARDWARE_SCHEMA_VERSION = 1; + +export const FARCLIP_FLOOR = 777; +export const FARCLIP_CEILING = 3000; + +const VIDEO_CLASS_GUID = '{4d36e968-e325-11ce-bfc1-08002be10318}'; + +const getVramMb = (): Promise<{ + mb: number | null; + source: HardwareInfo['vramSource']; +}> => { + if (os.platform() !== 'win32') + return Promise.resolve({ mb: null, source: 'none' }); + + const script = [ + '$ErrorActionPreference = "Stop"', + `$base = 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\${VIDEO_CLASS_GUID}'`, + '$max = [int64]0', + 'Get-ChildItem $base -ErrorAction SilentlyContinue | ForEach-Object {', + ' $p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue', + ' $v = [int64]0', + " if ($p.'HardwareInformation.qwMemorySize') { $v = [int64]$p.'HardwareInformation.qwMemorySize' }", + " elseif ($p.'HardwareInformation.MemorySize') {", + " $m = $p.'HardwareInformation.MemorySize'", + ' if ($m -is [byte[]]) { $v = [int64][System.BitConverter]::ToUInt32($m, 0) } else { $v = [int64]$m }', + ' }', + ' if ($v -gt $max) { $max = $v }', + '}', + 'Write-Output $max' + ].join('\n'); + const encoded = Buffer.from(script, 'utf16le').toString('base64'); + + return new Promise(resolve => { + let settled = false; + const finish = (mb: number | null, source: HardwareInfo['vramSource']) => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve({ mb, source }); + }; + + const child = spawn( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], + { windowsHide: true } + ); + + const timer = setTimeout(() => { + child.kill(); + Logger.warn('VRAM detection timed out'); + finish(null, 'none'); + }, 8000); + + let stdout = ''; + child.stdout.on('data', d => (stdout += String(d))); + child.on('error', e => { + Logger.warn('VRAM detection failed to launch PowerShell', e); + finish(null, 'none'); + }); + child.on('exit', code => { + const bytes = Number(stdout.trim()); + if (code === 0 && Number.isFinite(bytes) && bytes > 0) + finish(Math.round(bytes / 1024 / 1024), 'registry'); + else finish(null, 'none'); + }); + }); +}; + +const getGpuModel = async (): Promise => { + try { + const info = (await app.getGPUInfo('complete')) as { + auxAttributes?: { glRenderer?: string }; + gpuDevice?: { active?: boolean; vendorId?: number; deviceId?: number }[]; + }; + const renderer = info?.auxAttributes?.glRenderer?.trim(); + if (renderer) return renderer; + const active = info?.gpuDevice?.find(d => d.active) ?? info?.gpuDevice?.[0]; + if (active) return `vendor ${active.vendorId} device ${active.deviceId}`; + } catch (e) { + Logger.warn('GPU info detection failed', e); + } + return 'unknown'; +}; + +export const detectHardware = async (): Promise => { + const cpus = os.cpus(); + const [vram, gpuModel] = await Promise.all([getVramMb(), getGpuModel()]); + + const info: HardwareInfo = { + totalRamMb: Math.round(os.totalmem() / 1024 / 1024), + cpuCores: cpus.length, + cpuModel: cpus[0]?.model?.trim() || 'unknown', + gpuModel, + vramMb: vram.mb, + vramSource: vram.source, + detectedAt: new Date().toISOString(), + schemaVersion: HARDWARE_SCHEMA_VERSION + }; + Logger.info('Detected hardware', info); + return info; +}; + +const clampFarClip = (n: number) => + Math.min(FARCLIP_CEILING, Math.max(FARCLIP_FLOOR, Math.round(n))); + +export const recommendFarClip = (hw: HardwareInfo | null): number => { + if (!hw) return clampFarClip(1000); + + const ramGb = hw.totalRamMb / 1024; + const cores = hw.cpuCores; + const vramTrusted = hw.vramSource !== 'none' && hw.vramMb != null; + const vramGb = vramTrusted ? (hw.vramMb as number) / 1024 : null; + + if (ramGb < 6 || cores <= 2) return clampFarClip(FARCLIP_FLOOR); + + if (vramGb === null) + return clampFarClip(ramGb >= 8 && cores >= 4 ? 1500 : 1000); + + if (ramGb < 8 || vramGb < 2) return clampFarClip(1000); + if (ramGb >= 32 && vramGb >= 8 && cores >= 8) return clampFarClip(3000); + if (ramGb >= 16 && vramGb >= 4 && cores >= 6) return clampFarClip(2200); + if (ramGb >= 8 && vramGb >= 2 && cores >= 4) return clampFarClip(1500); + return clampFarClip(1000); +}; diff --git a/src/main/modules/localePatch.ts b/src/main/modules/localePatch.ts new file mode 100644 index 0000000..c1b5023 --- /dev/null +++ b/src/main/modules/localePatch.ts @@ -0,0 +1,60 @@ +import path from 'node:path'; + +import fs from 'fs-extra'; +import { + SFileOpenArchive, + SFileCloseArchive, + SFileHasFile +} from 'stormlib-node'; +import { STREAM_FLAG } from 'stormlib-node/dist/enums'; +import Logger from 'electron-log/main'; + +import Preferences from './preferences'; + +// old installs may have a copied patch-.mpq that overrides patch-5; sweep by marker +const ALL_LETTERS = 'BCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); +const MARKER = 'octolocale.marker'; + +const patchFile = (dataDir: string, letter: string) => + path.join(dataDir, `patch-${letter}.mpq`); + +const isOurPatch = (mpqPath: string): boolean => { + if (!fs.existsSync(mpqPath)) return false; + try { + const h = SFileOpenArchive(mpqPath, STREAM_FLAG.READ_ONLY); + try { + return SFileHasFile(h, MARKER); + } finally { + SFileCloseArchive(h); + } + } catch { + return false; + } +}; + +// remove locale patches we copied in (marker-carrying archives only); never throws +export const removeLegacyLocalePatches = async ( + clientDir: string | undefined +): Promise => { + if (!clientDir) return; + const dataDir = path.join(clientDir, 'Data'); + if (!(await fs.pathExists(dataDir))) return; + + for (const letter of ALL_LETTERS) { + const f = patchFile(dataDir, letter); + if (!isOurPatch(f)) continue; + try { + await fs.remove(f); + Logger.log(`Removed the retired locale patch patch-${letter}.mpq`); + } catch (e) { + Logger.error(`Could not remove patch-${letter}.mpq`, e); + } + } + + // clear the stale tracking keys + if (Preferences.data.localePatchLetter || Preferences.data.localePatchLocale) + Preferences.data = { + localePatchLetter: undefined, + localePatchLocale: undefined + }; +}; diff --git a/src/main/modules/mods.ts b/src/main/modules/mods.ts new file mode 100644 index 0000000..d59abd3 --- /dev/null +++ b/src/main/modules/mods.ts @@ -0,0 +1,665 @@ +import path from 'path'; +import { createHash } from 'crypto'; + +import fs from 'fs-extra'; +import fetch from 'node-fetch'; +import AdmZip from 'adm-zip'; +import * as tar from 'tar'; +import Logger from 'electron-log/main'; + +import { + MODS, + DEFAULT_ENABLED_MODS, + type ModEntry, + type ModId, + getMod +} from '~common/mods'; +import { type ModState } from '~common/schemas'; + +import Preferences from './preferences'; +import { isTorrentMode } from './aria2'; +import Observable from './observable'; +import Updater from './updater'; +import { addDll, removeDll, listDlls } from './dllsTxt'; +import { enumerateDisplays } from './displays'; + +const MOD_DOWNLOAD_TIMEOUT_MS = 60_000; + +/** Files a mod installs on disk. */ +const modTargetFiles = (m: ModEntry): string[] => { + if (m.source.kind === 'directFile') return [m.source.assetName]; + if (m.source.kind === 'archive') return Object.values(m.source.extractMap); + return []; +}; + +// client-shipped DLLs that aren't injectable mods; not counted as custom mods +const RESERVED_DLLS = new Set([ + 'ace.dll', + 'divxdecoder.dll', + 'discordoverlay.dll', + 'discord_game_sdk.dll', + 'dbghelp.dll', + 'fmod.dll', + 'ijl15.dll', + 'sdl.dll', + 'scan.dll', + 'unicows.dll', + 'zlib1.dll' +]); + +// files owned by an active built-in mod; a disabled mod's files are fair game to add by hand +const KNOWN_DLLS = new Set( + MODS.filter(m => !m.disabled) + .flatMap(m => [m.registerInDllsTxt, ...modTargetFiles(m)]) + .filter((f): f is string => !!f) + .map(f => f.toLowerCase()) +); + +const AV_ERROR = + 'Windows Defender blocked this download. Use "Allow through antivirus" and apply again.'; + +const looksLikeAvBlock = (msg: string) => + /windows defender|virus|potentially unwanted/i.test(msg); + +export type ModRowStatus = { + id: ModId; + name: string; + description: string; + repoUrl: string; + recommended: boolean; + requires: ModId[]; + enabled: boolean; + ignoreUpdates: boolean; + installedVersion?: string; + latestVersion: string; + state: 'idle' | 'downloading' | 'installing' | 'uninstalling' | 'error'; + progress?: number; + error?: string; +}; + +export type CustomMod = { name: string; enabled: boolean }; + +export type ModsStatus = { + state: 'verifying' | 'idle' | 'busy'; + dirty: boolean; + mods: ModRowStatus[]; + custom: CustomMod[]; + // enabled mods whose files are missing (AV quarantine or incomplete sync) + missingFiles: string[]; +}; + +class ModsClass extends Observable { + protected _value: ModsStatus = { + state: 'verifying', + dirty: false, + mods: [], + custom: [], + missingFiles: [] + }; + + // staged custom-DLL toggles, keyed lower-case; #customApplied mirrors dlls.txt + #customDesired = new Map(); + #customApplied = new Map(); + #customNames = new Map(); + + get status(): ModsStatus { + return this._value; + } + + #initialRow(m: ModEntry): ModRowStatus { + const state = Preferences.data?.mods?.[m.id]; + return { + id: m.id, + name: m.name, + description: m.description, + repoUrl: m.repoUrl, + recommended: !!m.recommended, + requires: m.requires ?? [], + enabled: !!state?.enabled, + ignoreUpdates: !!state?.ignoreUpdates, + installedVersion: state?.installedVersion, + latestVersion: m.version, + state: 'idle' + }; + } + + #patchRow(id: ModId, patch: Partial) { + this._value = { + ...this._value, + mods: this._value.mods.map(r => (r.id === id ? { ...r, ...patch } : r)) + }; + this._value = { ...this._value, dirty: this.#computeDirty() }; + this._notifyObservers(); + } + + #computeDirty(): boolean { + if (this.#customDesired.size > 0) return true; + return this._value.mods.some(r => { + const wantInstalled = r.enabled; + const isInstalled = !!r.installedVersion; + if (wantInstalled !== isInstalled) return true; + if ( + r.installedVersion && + r.installedVersion !== r.latestVersion && + !r.ignoreUpdates + ) + return true; + return false; + }); + } + + load() { + this._value = { + state: 'verifying', + dirty: false, + mods: MODS.filter(m => !m.disabled).map(m => this.#initialRow(m)), + custom: this._value.custom, + missingFiles: [] + }; + } + + // DLLs in the client dir we neither ship nor own + async #detectCustomDlls(clientDir: string): Promise { + const inDllsTxt = await listDlls(clientDir); + const enabled = new Set(inDllsTxt.map(n => n.toLowerCase())); + const found = new Map(); + const consider = (name: string) => { + const lc = name.toLowerCase(); + if (RESERVED_DLLS.has(lc) || KNOWN_DLLS.has(lc) || found.has(lc)) return; + found.set(lc, name); + }; + for (const f of await fs.readdir(clientDir).catch(() => [] as string[])) + if (/\.dll$/i.test(f)) consider(f); + inDllsTxt.forEach(consider); + const names = [...found.values()].sort((a, b) => a.localeCompare(b)); + this.#customApplied = new Map( + names.map(n => [n.toLowerCase(), enabled.has(n.toLowerCase())]) + ); + this.#customNames = new Map(names.map(n => [n.toLowerCase(), n])); + // drop staged changes for DLLs no longer present + const present = new Set(names.map(n => n.toLowerCase())); + for (const lc of [...this.#customDesired.keys()]) + if (!present.has(lc)) this.#customDesired.delete(lc); + return names.map(name => { + const lc = name.toLowerCase(); + return { + name, + enabled: this.#customDesired.has(lc) + ? !!this.#customDesired.get(lc) + : !!this.#customApplied.get(lc) + }; + }); + } + + // flush staged custom-DLL changes to dlls.txt; a failed write stays staged (still pending) + async #applyCustomDlls(clientDir: string) { + for (const [lc, enabled] of [...this.#customDesired]) { + const name = this.#customNames.get(lc) ?? lc; + try { + await (enabled ? addDll(clientDir, name) : removeDll(clientDir, name)); + this.#customDesired.delete(lc); + } catch (e) { + Logger.warn(`custom dll apply failed for ${name}`, e); + } + } + } + + async #syncPreferredMonitor(clientDir: string) { + const vmmfDll = path.join(clientDir, 'VanillaMultiMonitorFix.dll'); + if (!(await fs.pathExists(vmmfDll))) return; + + const vmmfCfg = path.join(clientDir, 'VMMFix_preferred_monitor.txt'); + const existsCfg = await fs.pathExists(vmmfCfg); + const current = existsCfg + ? Number( + await fs + .readFile(vmmfCfg, 'utf8') + .then(s => s.trim()) + .catch(() => '') + ) + : NaN; + const hasCurrent = Number.isInteger(current); + const ours = Preferences.data?.vmmfWrittenIndex; + + const devices = await enumerateDisplays(); + const usable = devices?.filter(d => d.attached && d.width > 0); + if (!devices || !usable?.length) { + Logger.warn('Could not enumerate displays; preferred monitor unchanged'); + return; + } + const primary = usable.find(d => d.primary) ?? usable[0]; + + if (hasCurrent && ours === undefined) { + const pinned = devices.find(d => d.index === current); + const broken = !pinned || !pinned.attached || !pinned.primary; + if (!broken) { + Preferences.data = { vmmfWrittenIndex: current }; + Logger.info(`Adopting existing preferred monitor ${current} as chosen`); + return; + } + Logger.warn( + `Preferred monitor ${current} (${ + pinned ? pinned.deviceName : 'missing' + }) is ${ + !pinned || !pinned.attached + ? 'not attached' + : 'not the primary display' + }; healing to ${primary.index}` + ); + } else if (hasCurrent && current !== ours) { + Logger.info( + `Preferred monitor ${current} was set manually; leaving it alone` + ); + Preferences.data = { vmmfWrittenIndex: current }; + return; + } else if (hasCurrent && current === primary.index) { + return; + } + + await fs + .writeFile(vmmfCfg, `${primary.index}\n`, 'utf8') + .then(() => { + Preferences.data = { vmmfWrittenIndex: primary.index }; + Logger.info( + `Preferred monitor set to ${primary.index} (${primary.deviceName} ${primary.width}x${primary.height})` + ); + }) + .catch(e => Logger.warn('Failed to write preferred monitor', e)); + } + + async verify() { + this.load(); + this._notifyObservers(); + + const clientDir = Preferences.data?.clientDir; + + if (clientDir) { + await this.#syncPreferredMonitor(clientDir); + } + + const missing: string[] = []; + for (const m of MODS) { + // disabled mods: leave dlls.txt and installed state untouched + if (m.disabled) continue; + + const state = Preferences.data?.mods?.[m.id]; + let installedVersion = state?.installedVersion; + + // torrent mode: DLLs ship in the client; a missing file goes to `missing`, not dirty + if (isTorrentMode()) { + const files = modTargetFiles(m); + const present = + !!clientDir && + files.length > 0 && + ( + await Promise.all( + files.map(rel => fs.pathExists(path.join(clientDir, rel))) + ) + ).every(Boolean); + const enabled = state?.enabled ?? DEFAULT_ENABLED_MODS.includes(m.id); + installedVersion = enabled ? m.version : undefined; + if (enabled && files.length > 0 && !present) missing.push(m.name); + // only point dlls.txt at a file actually on disk + if (clientDir && m.registerInDllsTxt) + await (present && enabled + ? addDll(clientDir, m.registerInDllsTxt) + : removeDll(clientDir, m.registerInDllsTxt) + ).catch(e => Logger.warn(`dlls.txt update failed for ${m.id}`, e)); + this.#patchRow(m.id, { + installedVersion, + latestVersion: m.version, + enabled, + ignoreUpdates: true + }); + continue; + } + + if (clientDir && installedVersion) { + const filesPresent = await Promise.all( + (state?.installedFiles ?? []).map(rel => + fs.pathExists(path.join(clientDir, rel)) + ) + ); + if (state?.installedFiles?.length && !filesPresent.every(Boolean)) { + installedVersion = undefined; + await this.#savePref(m.id, { + enabled: state?.enabled ?? false, + installedVersion: undefined, + installedFiles: [], + ignoreUpdates: state?.ignoreUpdates ?? false + }); + } + } + + if (clientDir && m.registerInDllsTxt) + await (installedVersion + ? addDll(clientDir, m.registerInDllsTxt) + : removeDll(clientDir, m.registerInDllsTxt) + ).catch(() => {}); + + this.#patchRow(m.id, { + installedVersion, + latestVersion: m.version, + enabled: !!state?.enabled, + ignoreUpdates: !!state?.ignoreUpdates + }); + } + + this._value = { + ...this._value, + state: 'idle', + dirty: this.#computeDirty(), + custom: clientDir ? await this.#detectCustomDlls(clientDir) : [], + missingFiles: missing + }; + this._notifyObservers(); + } + + async toggleCustom(name: string, enabled: boolean) { + const clientDir = Preferences.data?.clientDir; + if (!clientDir) return; + // stage; matching dlls.txt clears the pending change + const lc = name.toLowerCase(); + if (enabled === !!this.#customApplied.get(lc)) + this.#customDesired.delete(lc); + else this.#customDesired.set(lc, enabled); + this._value = { + ...this._value, + custom: await this.#detectCustomDlls(clientDir) + }; + this._value = { ...this._value, dirty: this.#computeDirty() }; + this._notifyObservers(); + } + + async addCustomDll( + srcPath: string + ): Promise<{ ok: boolean; error?: string }> { + const clientDir = Preferences.data?.clientDir; + if (!clientDir) return { ok: false, error: 'No game folder is set.' }; + const name = path.basename(srcPath); + if (!/\.dll$/i.test(name)) + return { ok: false, error: 'Please choose a .dll file.' }; + const lc = name.toLowerCase(); + if (RESERVED_DLLS.has(lc) || KNOWN_DLLS.has(lc)) + return { + ok: false, + error: `${name} is a built-in file and can't be added as a custom mod.` + }; + try { + const dest = path.join(clientDir, name); + if (path.resolve(srcPath) !== path.resolve(dest)) + await fs.copy(srcPath, dest, { overwrite: true }); + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : String(e) }; + } + // stage enabled; Apply writes dlls.txt + this.#customDesired.set(name.toLowerCase(), true); + this._value = { + ...this._value, + custom: await this.#detectCustomDlls(clientDir) + }; + this._value = { ...this._value, dirty: this.#computeDirty() }; + this._notifyObservers(); + return { ok: true }; + } + + async toggle(id: ModId, enabled: boolean) { + const cur = Preferences.data?.mods?.[id]; + await this.#savePref(id, { + enabled, + installedVersion: cur?.installedVersion, + installedFiles: cur?.installedFiles ?? [], + ignoreUpdates: cur?.ignoreUpdates ?? false + }); + this.#patchRow(id, { enabled }); + } + + async setIgnoreUpdates(id: ModId, ignore: boolean) { + const cur = Preferences.data?.mods?.[id]; + await this.#savePref(id, { + enabled: cur?.enabled ?? false, + installedVersion: cur?.installedVersion, + installedFiles: cur?.installedFiles ?? [], + ignoreUpdates: ignore + }); + this.#patchRow(id, { ignoreUpdates: ignore }); + } + + async applyAll(opts: { repairOnly?: boolean } = {}) { + const clientDir = Preferences.data?.clientDir; + if (!clientDir) { + Logger.warn('No clientDir set; cannot apply mods.'); + return; + } + // don't commit a mod set with an unmet dependency; dirty stays set. repair is exempt. + if (!opts.repairOnly) { + const enabledIds = new Set( + this._value.mods.filter(r => r.enabled).map(r => r.id) + ); + const missingDeps = [ + ...new Set( + this._value.mods + .filter(r => r.enabled) + .flatMap(r => r.requires.filter(dep => !enabledIds.has(dep))) + ) + ]; + if (missingDeps.length) { + Logger.warn( + `Not applying mods: unmet dependencies ${missingDeps.join(', ')}` + ); + return; + } + } + // commit the player's own DLL toggles first + await this.#applyCustomDlls(clientDir); + // torrent mode: mods ship in the client; just reconcile dlls.txt + if (isTorrentMode()) { + await this.verify(); + return; + } + if (this._value.state === 'busy') { + Logger.warn('applyAll already running; ignoring re-entrant call.'); + return; + } + await this.verify(); + this._value = { ...this._value, state: 'busy' }; + this._notifyObservers(); + + const queue = [...this._value.mods]; + queue.sort((a, b) => { + if (a.id === 'vanillaFixes') return -1; + if (b.id === 'vanillaFixes') return 1; + return 0; + }); + + const failures = new Map(); + for (const row of queue) { + const m = getMod(row.id); + if (!m) continue; + + const wantInstalled = row.enabled; + const isInstalled = !!row.installedVersion; + const updateAvailable = + isInstalled && + row.installedVersion !== row.latestVersion && + !row.ignoreUpdates; + + try { + if (wantInstalled && !isInstalled) { + await this.#install(m); + } else if (!wantInstalled && isInstalled) { + await this.#uninstall(m); + } else if (wantInstalled && updateAvailable && !opts.repairOnly) { + await this.#uninstall(m); + await this.#install(m); + } + } catch (e) { + Logger.error(`Failed to apply ${m.id}:`, e); + const msg = e instanceof Error ? e.message : String(e); + failures.set(m.id, looksLikeAvBlock(msg) ? AV_ERROR : msg); + } + } + + this._value = { ...this._value, state: 'idle' }; + await this.verify(); + for (const [id, error] of failures) + this.#patchRow(id, { state: 'error', error }); + await Updater.verify(); + } + + async #install(m: ModEntry) { + // In torrent mode the mod binaries ship with the client; nothing is fetched. + if (isTorrentMode()) return; + const clientDir = Preferences.data?.clientDir; + if (!clientDir) throw new Error('No client dir'); + if (m.source.kind === 'managed') return; + + Logger.info(`Installing mod ${m.id}...`); + this.#patchRow(m.id, { + state: 'downloading', + progress: 0, + error: undefined + }); + + const written: string[] = []; + const missing: string[] = []; + + if (m.source.kind === 'directFile') { + const dest = path.join(clientDir, m.source.assetName); + await this.#downloadTo(m.source.url, dest, m.source.sha256); + written.push(m.source.assetName); + } else if (m.source.kind === 'archive') { + const scratch = path.join(clientDir, '.octolauncher-tmp'); + await fs.ensureDir(scratch); + const tmp = path.join( + scratch, + `${m.id}-${Date.now()}.${m.source.format}` + ); + await this.#downloadTo(m.source.url, tmp, m.source.sha256); + this.#patchRow(m.id, { state: 'installing' }); + + const map = m.source.extractMap; + if (m.source.format === 'zip') { + const zip = new AdmZip(tmp); + const entries = zip.getEntries(); + for (const [src, dst] of Object.entries(map)) { + const entry = entries.find(e => e.entryName === src); + if (!entry) { + missing.push(src); + continue; + } + const target = path.join(clientDir, dst); + await fs.ensureDir(path.dirname(target)); + await fs.writeFile(target, entry.getData()); + written.push(dst); + } + } else { + const stagingDir = path.join(scratch, `${m.id}-${Date.now()}-extract`); + await fs.ensureDir(stagingDir); + await tar.x({ file: tmp, cwd: stagingDir }); + for (const [src, dst] of Object.entries(map)) { + const srcPath = path.join(stagingDir, src); + if (!(await fs.pathExists(srcPath))) { + missing.push(src); + continue; + } + const target = path.join(clientDir, dst); + await fs.ensureDir(path.dirname(target)); + await fs.copy(srcPath, target); + written.push(dst); + } + await fs.remove(stagingDir).catch(() => {}); + } + await fs.remove(tmp).catch(() => {}); + } + + if (missing.length) + throw new Error( + `${m.name}: download is missing expected file(s): ${missing.join(', ')}` + ); + + if (m.registerInDllsTxt) { + await addDll(clientDir, m.registerInDllsTxt); + } + + await this.#savePref(m.id, { + enabled: true, + installedVersion: m.version, + installedFiles: written, + ignoreUpdates: Preferences.data?.mods?.[m.id]?.ignoreUpdates ?? false + }); + + this.#patchRow(m.id, { + state: 'idle', + installedVersion: m.version, + progress: 1 + }); + } + + async #uninstall(m: ModEntry) { + const clientDir = Preferences.data?.clientDir; + if (!clientDir) throw new Error('No client dir'); + if (m.source.kind === 'managed') return; + + Logger.info(`Uninstalling mod ${m.id}...`); + this.#patchRow(m.id, { state: 'uninstalling', error: undefined }); + + const cur = Preferences.data?.mods?.[m.id]; + const files = cur?.installedFiles ?? []; + + for (const rel of files) { + const fullPath = path.join(clientDir, rel); + await fs + .remove(fullPath) + .catch(err => Logger.warn(`Couldn't remove ${fullPath}:`, err)); + } + + if (m.registerInDllsTxt) { + await removeDll(clientDir, m.registerInDllsTxt); + } + + await this.#savePref(m.id, { + enabled: cur?.enabled ?? false, + installedVersion: undefined, + installedFiles: [], + ignoreUpdates: cur?.ignoreUpdates ?? false + }); + + this.#patchRow(m.id, { state: 'idle', installedVersion: undefined }); + } + + async #downloadTo(url: string, dest: string, sha256?: string) { + const res = await fetch(url, { + headers: { 'User-Agent': 'OctoLauncher' }, + timeout: MOD_DOWNLOAD_TIMEOUT_MS + }); + if (!res.ok) throw new Error(`Download failed ${res.status}: ${url}`); + await fs.ensureDir(path.dirname(dest)); + const buf = await res.arrayBuffer(); + + if (sha256) { + const got = createHash('sha256').update(Buffer.from(buf)).digest('hex'); + if (got !== sha256.toLowerCase()) + throw new Error( + `Checksum mismatch for ${path.basename( + dest + )}: expected ${sha256}, got ${got}. Refusing to install.` + ); + } + + await fs.writeFile(dest, Buffer.from(buf)); + if (!(await fs.pathExists(dest))) + throw new Error( + `Downloaded file disappeared after writing: ${path.basename(dest)}. ` + + 'This is often Windows Defender quarantine; if so, use "Allow through antivirus" and apply again.' + ); + } + + async #savePref(id: ModId, state: ModState) { + const allMods = { ...(Preferences.data?.mods ?? {}), [id]: state }; + Preferences.data = { mods: allMods }; + } +} + +const Mods = new ModsClass(); +export default Mods; diff --git a/src/main/modules/observable.ts b/src/main/modules/observable.ts new file mode 100644 index 0000000..6b11dde --- /dev/null +++ b/src/main/modules/observable.ts @@ -0,0 +1,37 @@ +import { observable } from '@trpc/server/observable'; + +type Func = (arg: T) => void; + +abstract class Observable { + private _listeners: Func[] = []; + + protected abstract _value: T; + + protected _notifyObservers(v = this._value) { + this._listeners = this._listeners.filter(l => { + try { + l(v); + return true; + } catch (err) { + console.error('Observer threw, removing listener', err); + return false; + } + }); + } + + observe() { + return observable(e => { + e.next(this._value); + this._listeners.push(e.next); + return () => { + this._listeners = this._listeners.filter(v => v !== e.next); + }; + }); + } + + clearObservers() { + this._listeners = []; + } +} + +export default Observable; diff --git a/src/main/modules/patcher.ts b/src/main/modules/patcher.ts new file mode 100644 index 0000000..b381f16 --- /dev/null +++ b/src/main/modules/patcher.ts @@ -0,0 +1,467 @@ +import path from 'path'; + +import { screen } from 'electron'; +import fs from 'fs-extra'; +import Logger from 'electron-log/main'; + +import Preferences from '~main/modules/preferences'; +import { ConfigWtfSchema, type PreferencesSchema } from '~common/schemas'; +import { isNotUndef } from '~common/utils'; +import { readPristineWow } from '~main/modules/aria2'; +import { enumerateDisplays } from '~main/modules/displays'; + +const Servers = { + live: { + realmList: 'octowow.st', + patchList: 'octowow.st', + realmName: 'OctoWoW' + }, + ptr: { + realmList: import.meta.env.MAIN_VITE_PTR_REALMLIST || 'octowow.st', + patchList: import.meta.env.MAIN_VITE_PTR_REALMLIST || 'octowow.st', + realmName: 'OctoWoW PTR' + } +} as const; + +const LOCALES = { + enUS: { tag: 'enUS', index: 0 }, + deDE: { tag: 'deDE', index: 3 }, + zhCN: { tag: 'zhCN', index: 4 }, + ruRU: { tag: 'ruRU', index: 5 }, + esES: { tag: 'esES', index: 6 }, + ptBR: { tag: 'ptBR', index: 7 } +} as const satisfies Record< + PreferencesSchema['locale'], + { tag: string; index: number } +>; + +const LOCALE_NAMES = [ + 'enUS', + 'koKR', + 'frFR', + 'deDE', + 'zhCN', + 'zhTW', + 'esES', + 'xxYY' +] as const; + +const localeNameOffset = (index: number) => 0x45591c - index * 8; + +const carrierName = (index: number) => LOCALE_NAMES[index]; + +type TweakKey = + | { synthetic?: false; key: keyof PreferencesSchema['config'] } + | { synthetic: true; key: string }; + +type Tweak = TweakKey & { + default?: unknown; + forced?: boolean; +} & ( + | { + type: 'bytes'; + tweaks: [number, number[], number[]?][]; + } + | { + type: 'int8' | 'uint16' | 'float'; + offset: number; + value?: number; + } + ); + +const hex = (bytes: number[]) => + bytes.map(b => b.toString(16).padStart(2, '0')).join(' '); + +export const patchExecutable = async () => { + Logger.log('Patching WoW.exe...'); + + const { clientDir, config, locale } = Preferences.data; + if (!clientDir) return; + const exePath = path.join(clientDir, 'WoW.exe'); + + try { + Logger.log('Reading clean WoW.exe base...'); + const buffer = await readPristineWow(clientDir); + + const loc = LOCALES[locale]; + + const Tweaks = [ + { + key: 'largeAddress', + type: 'uint16', + offset: 0x126, + value: buffer.readUint16LE(0x126) | 0x20, + default: false + }, + { key: 'farClip', type: 'float', offset: 0x40fed8 }, + { + key: 'fieldOfView', + type: 'float', + offset: 0x4089b4, + value: (config.fieldOfView ?? 1) * (Math.PI / 180), + default: 90 + }, + { key: 'frillDistance', type: 'float', offset: 0x467958 }, + { + key: 'soundInBackground', + type: 'int8', + offset: 0x3a4869, + value: config.soundInBackground ? 0x27 : 0x14, + default: false + }, + { + key: 'alwaysAutoLoot', + type: 'bytes', + tweaks: [ + [0x0c1ecf, [0x75]], + [0x0c2b25, [0x75]] + ] + }, + { key: 'nameplateRange', type: 'float', offset: 0x40c448 }, + { key: 'cameraDistance', type: 'float', offset: 0x4089a4 }, + { + synthetic: true, + key: 'skillUiGateHijack', + type: 'bytes', + default: true, + forced: true, + tweaks: [ + [ + 0x002ddf90, + [ + 0x55, 0x8b, 0xec, 0x83, 0xec, 0x08, 0x53, 0x56, 0x57, 0x8b, 0x3d, + 0x60, 0xab, 0xce, 0x00, 0x83, 0xff, 0xff, 0x89, 0x55, 0xfc, 0x89, + 0x4d, 0xf8, 0x74, 0x79, 0x8b, 0x75, 0x08, 0x8b, 0x15, 0x58, 0xab, + 0xce, 0x00, 0x8b, 0xc7, 0x23, 0xc6, 0x8d, 0x04, 0x40, 0x8b, 0x4c, + 0x82, 0x08, 0xf6, 0xc1, 0x01, 0x8d, 0x44, 0x82, 0x04, 0x75, 0x04, + 0x85, 0xc9, 0x75, 0x05, 0x33, 0xc9, 0x8d, 0x49, 0x00, 0xf6, 0xc1, + 0x01, 0x75, 0x4e, 0x85, 0xc9, 0x74, 0x4a, 0x39, 0x31, 0x74, 0x13, + 0x8b, 0xc7, 0x23, 0xc6, 0x8d, 0x04, 0x40, 0x8d, 0x04, 0x82, 0x8b, + 0x00, 0x03, 0xc1, 0x8b, 0x48, 0x04, 0xeb, 0xe0, 0x8b, 0x59, 0x1c, + 0x8b, 0x71, 0x18, 0x33, 0xff, 0x85, 0xdb, 0x7e, 0x27, 0x8d, 0x64, + 0x24, 0x00, 0x8b, 0x4e, 0x0c, 0x8b, 0x56, 0x08, 0x6a, 0x00, 0x6a, + 0x00, 0x51, 0x8b, 0x4d, 0xf8, 0x52, 0x8b, 0x55, 0xfc, 0xe8, 0xb9, + 0xfd, 0xff, 0xff, 0x84, 0xc0, 0x75, 0x13, 0x47, 0x83, 0xc6, 0x20, + 0x3b, 0xfb, 0x7c, 0xdd, 0x5f, 0x5e, 0x33, 0xc0, 0x5b, 0x8b, 0xe5, + 0x5d, 0xc2, 0x04, 0x00, 0x5f, 0x8b, 0xc6, 0x5e, 0x5b, 0x8b, 0xe5, + 0x5d, 0xc2, 0x04, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 + ] + ] + ] + }, + { + synthetic: true, + key: 'octowowUrlAllowlist', + type: 'bytes', + default: true, + forced: true, + tweaks: [ + [ + 0x45ccd8, + [ + 0x6f, 0x63, 0x74, 0x6f, 0x77, 0x6f, 0x77, 0x2e, 0x73, 0x74, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 + ] + ] + ] + }, + { + synthetic: true, + key: 'localeTag', + type: 'bytes', + default: true, + forced: true, + tweaks: [ + [ + 0x1b2115, + [0xb8, ...Buffer.from(carrierName(loc.index), 'latin1').reverse()], + [0xa1, 0xa4, 0xa2, 0xc2, 0x00] + ] + ] + }, + { + synthetic: true, + key: 'localeIndex', + type: 'bytes', + default: true, + forced: true, + tweaks: [ + [ + 0x253c, + [0xbe, loc.index, 0x00, 0x00, 0x00, 0xeb, 0x1f], + [0x33, 0xf6, 0x8b, 0xff, 0x8b, 0x04, 0xb5] + ] + ] + }, + { + synthetic: true, + key: 'localeName', + type: 'bytes', + default: true, + forced: true, + tweaks: [ + [ + localeNameOffset(loc.index), + [...Buffer.from(loc.tag, 'latin1')], + [...Buffer.from(LOCALE_NAMES[loc.index], 'latin1')] + ] + ] + } + ] satisfies Tweak[]; + + Tweaks.forEach(t => { + const val = t.synthetic + ? t.default + : config[t.key] ?? t.default ?? ConfigWtfSchema.parse({})[t.key]; + + Logger.log(`Applying "${t.key}" patch with value: ${val}`); + if (t.type === 'float') { + buffer.writeFloatLE(t.value ?? (val as number), t.offset); + } else if (t.type === 'int8') { + buffer.writeInt8(t.value ?? (val as number), t.offset); + } else if (t.type === 'uint16') { + if (!t.forced && !val) return; + buffer.writeUInt16LE(t.value ?? (val as number), t.offset); + } else if (t.type === 'bytes') { + if (!t.forced && !val) return; + t.tweaks.forEach( + ([offset, bytes, expect]: [number, number[], number[]?]) => { + if (expect) { + const current = buffer.subarray(offset, offset + expect.length); + if (current.equals(Buffer.from(bytes))) return; + if (!current.equals(Buffer.from(expect))) + throw new Error( + `"${t.key}" expected [${hex(expect)}] at 0x${offset.toString( + 16 + )} ` + + `but found [${hex([ + ...current + ])}]; refusing to patch WoW.exe` + ); + } + const written = Buffer.from(bytes).copy(buffer, offset); + if (written !== bytes.length) + Logger.error( + `"${t.key}" wrote ${written}/${bytes.length} bytes at ` + + `0x${offset.toString(16)}: past end of file (${ + buffer.length + } bytes). ` + + 'This tweak is a no-op; the offset is probably a virtual address.' + ); + } + ); + } + }); + + await fs.writeFile(exePath, buffer); + Preferences.data = { patchedLocale: locale }; + Logger.log(`WoW.exe successfully patched (language: ${locale})`); + } catch (e) { + Logger.error('Failed to patch WoW.exe', e); + throw e instanceof Error ? e : new Error('Failed to patch WoW.exe'); + } +}; + +const repairResolution = async ( + clientDir: string, + current: string | undefined, + lastWritten: string | undefined +): Promise<{ gxResolution?: string }> => { + const devices = await enumerateDisplays(); + if (!devices?.length) return {}; + + const pinnedRaw = await fs + .readFile(path.join(clientDir, 'VMMFix_preferred_monitor.txt'), 'utf8') + .then(s => s.trim()) + .catch(() => ''); + const pinned = pinnedRaw ? Number(pinnedRaw) : NaN; + const target = + devices.find(d => d.index === pinned && d.attached) ?? + devices.find(d => d.primary && d.attached); + if (!target?.modes.length) return {}; + + const native = `${target.width}x${target.height}`; + if (!target.modes.includes(native)) return {}; + + let owned = !current || current === lastWritten; + + if (!owned && lastWritten === undefined && current) { + const width = Number(current.split('x')[0]); + if (Number.isFinite(width) && width * 2 < target.width) { + Logger.warn( + `gxResolution ${current} is far below ${target.deviceName}'s ${native} and predates resolution tracking; treating it as a client fallback` + ); + owned = true; + } + } + + if (!owned) return {}; + if (current === native) return {}; + + Logger.warn( + `gxResolution ${current ?? ''} is launcher-owned; correcting to ${ + target.deviceName + }'s ${native}` + ); + return { gxResolution: native }; +}; + +const applyRealmlist = async (clientDir: string, host: string) => { + const body = `set realmlist "${host}"\n`; + const write = async (target: string) => { + const tmp = `${target}.tmp`; + try { + await fs.writeFile(tmp, body); + await fs.move(tmp, target, { overwrite: true }); + } catch (e) { + await fs.remove(tmp).catch(() => undefined); + throw e; + } + }; + await write(path.join(clientDir, 'realmlist.wtf')); + const dataDir = path.join(clientDir, 'Data'); + if (await fs.pathExists(dataDir)) + for (const entry of await fs.readdir(dataDir)) { + const scoped = path.join(dataDir, entry, 'realmlist.wtf'); + if (!(await fs.pathExists(scoped))) continue; + try { + await write(scoped); + } catch (e) { + Logger.warn(`Could not rewrite ${scoped}: ${String(e)}`); + } + } +}; + +export const patchConfig = async (forceTweaks = false) => { + const { clientDir, config, locale } = Preferences.data; + if (!clientDir) return; + + const server: keyof typeof Servers = import.meta.env.MAIN_VITE_PTR_REALMLIST + ? 'ptr' + : 'live'; + + const configPath = path.join(clientDir, 'WTF', 'Config.wtf'); + await fs.ensureDir(path.dirname(configPath)); + const raw = (await fs.pathExists(configPath)) + ? await fs.readFile(configPath, { encoding: 'utf-8' }) + : ''; + + const configWtf = Object.fromEntries( + raw + .split(/\r?\n/) + .map(l => { + const [, k, v] = l.match(/SET (\w+) "(.*)"/) ?? []; + return !k || v === undefined ? undefined : [k, v]; + }) + .filter(isNotUndef) + ); + + const isFirstRun = Object.keys(configWtf).length === 0; + + const primaryDisplay = screen.getPrimaryDisplay(); + const scale = primaryDisplay.scaleFactor || 1; + const width = Math.round(primaryDisplay.bounds.width * scale); + const height = Math.round(primaryDisplay.bounds.height * scale); + + const seededResolution = `${width}x${height}`; + + const seed = isFirstRun + ? { + scriptMemory: 512000, + gxResolution: seededResolution, + gxColorBits: primaryDisplay.colorDepth, + gxDepthBits: primaryDisplay.colorDepth, + gxRefresh: 60, + gxMultisample: 8, + gxMultisampleQuality: 0, + gxTripleBuffer: 1, + anisotropic: 16, + frillDensity: 48, + fullAlpha: 1, + SmallCull: 0.01, + DistCull: 888.8, + shadowLevel: 0, + trilinear: 1, + specular: 1, + pixelShaders: 1, + M2UsePixelShaders: 1, + M2UseShaders: 1, + particleDensity: 1, + unitDrawDist: 300, + weatherDensity: 3, + movieSubtitle: 1, + minimapZoom: 0, + minimapInsideZoom: 0, + SoundZoneMusicNoDelay: 1, + gxWindow: 1, + gxMaximize: 1, + gxCursor: 1, + checkAddonVersion: 0, + farClip: config.farClip, + CameraDistanceMax: config.cameraDistance, + patchList: Servers[server].patchList, + realmName: Servers[server].realmName + } + : {}; + + const owned = { + locale: carrierName(LOCALES[locale].index), + patchList: configWtf['patchList'] ?? Servers[server].patchList, + realmName: configWtf['realmName'] ?? Servers[server].realmName, + hwDetect: 0, + BackgroundSound: config.soundInBackground ? 1 : 0 + }; + + const repaired = await repairResolution( + clientDir, + configWtf['gxResolution'], + Preferences.data.lastWrittenResolution + ); + + const parsed = { + ...seed, + ...configWtf, + ...repaired, + ...owned, + ...(forceTweaks + ? { farClip: config.farClip, CameraDistanceMax: config.cameraDistance } + : {}) + }; + + const body = Object.entries(parsed) + .filter(v => v[1] !== undefined && v[1] !== null) + .filter(([k]) => !/^realmlist$/i.test(k)) + .map(l => `SET ${l[0]} "${l[1]}"`) + .join('\n'); + const tmpPath = `${configPath}.tmp`; + await fs.writeFile(tmpPath, body); + await fs.move(tmpPath, configPath, { overwrite: true }); + + await applyRealmlist(clientDir, Servers[server].realmList); + + const chosen = + repaired.gxResolution ?? (isFirstRun ? seededResolution : undefined); + if (chosen && chosen !== Preferences.data.lastWrittenResolution) + Preferences.data = { lastWrittenResolution: chosen }; + + Logger.log('Config.wtf successfully patched'); +}; + +export const ensureDxvkConf = async (clientDir: string) => { + if (!(await fs.pathExists(path.join(clientDir, 'd3d9.dll')))) return; + const confPath = path.join(clientDir, 'dxvk.conf'); + if (await fs.pathExists(confPath)) return; + await fs.writeFile( + confPath, + [ + '# Cap the texture memory the 32-bit client believes it has so it cannot', + '# over-commit its address space (the common DXVK out-of-memory crash).', + 'd3d9.maxAvailableMemory = 2048', + 'd3d9.maxFrameLatency = 1', + 'dxvk.numCompilerThreads = 2', + 'dxvk.logLevel = none', + '' + ].join('\n') + ); + Logger.log('Wrote dxvk.conf'); +}; diff --git a/src/main/modules/preferences.ts b/src/main/modules/preferences.ts new file mode 100644 index 0000000..618c544 --- /dev/null +++ b/src/main/modules/preferences.ts @@ -0,0 +1,270 @@ +import path from 'path'; + +import fs from 'fs-extra'; +import { type z } from 'zod'; +import { app } from 'electron'; +import Logger from 'electron-log/main'; + +import { PreferencesSchema } from '~common/schemas'; +import { DEFAULT_ENABLED_MODS } from '~common/mods'; +import { omit } from '~common/utils'; +import { isTorrentMode } from '~main/modules/aria2'; + +const portableDir = process.env.PORTABLE_EXECUTABLE_DIR; + +const errCode = (e: unknown) => + e && typeof e === 'object' ? (e as NodeJS.ErrnoException).code : undefined; + +const LOCK_CODES = ['EPERM', 'EACCES', 'EBUSY', 'EMFILE', 'ENFILE']; +const isLocked = (e: unknown) => LOCK_CODES.includes(errCode(e) ?? ''); + +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const readJsonRetrying = async (file: string, attempts = 5) => { + for (let i = 0; ; i++) { + try { + return await fs.readJSON(file); + } catch (e) { + if (i >= attempts - 1 || !isLocked(e)) throw e; + await delay(60 * (i + 1)); + } + } +}; + +const renameRetrying = async (from: string, to: string, attempts = 5) => { + for (let i = 0; ; i++) { + try { + return await fs.rename(from, to); + } catch (e) { + if (i >= attempts - 1 || !isLocked(e)) throw e; + await delay(60 * (i + 1)); + } + } +}; + +const writeJsonAtomic = async (file: string, data: unknown) => { + const tmp = `${file}.tmp`; + await fs.writeJSON(tmp, data, { spaces: 2 }); + await renameRetrying(tmp, file); +}; + +const dropUndefined = (obj: T): Partial => + Object.fromEntries( + Object.entries(obj).filter(([, v]) => v !== undefined) + ) as Partial; + +abstract class Preferences { + static #data: z.infer; + static #writeChain: Promise = Promise.resolve(); + static #readOnly = false; + static #rememberedClientDir?: string; + static #freshInstall = false; + + static readonly userDataDir = process.env.PORTABLE_EXECUTABLE_DIR + ? path.join(process.env.PORTABLE_EXECUTABLE_DIR, '.launcher') + : app.getPath('userData'); + + static readonly #settingsPath = path.join( + Preferences.userDataDir, + 'settings.json' + ); + + static readonly #installPath = path.join( + Preferences.userDataDir, + 'install.json' + ); + + static get isFreshInstall() { + return this.#freshInstall; + } + + static async #detectFreshInstall() { + const [settings, install, pending] = await Promise.all([ + fs.pathExists(this.#settingsPath), + fs.pathExists(this.#installPath), + fs.pathExists(`${this.#settingsPath}.tmp`) + ]); + return !settings && !install && !pending; + } + + static #withFreshInstallDefaults(data: PreferencesSchema): PreferencesSchema { + if (!this.#freshInstall || Object.keys(data.mods).length) return data; + + const mods = { ...data.mods }; + for (const id of DEFAULT_ENABLED_MODS) + mods[id] = { enabled: true, installedFiles: [], ignoreUpdates: false }; + + Logger.info( + `Fresh install: enabling ${DEFAULT_ENABLED_MODS.join(', ')} by default` + ); + return { ...data, mods }; + } + + static async load() { + this.#freshInstall = await this.#detectFreshInstall(); + await fs.ensureDir(this.userDataDir); + const settingsPath = this.#settingsPath; + + let json: Record = {}; + try { + json = await readJsonRetrying(settingsPath); + } catch (e) { + if (isLocked(e)) { + this.#readOnly = true; + Logger.error( + `Could not read ${settingsPath} (${errCode(e)}); running on ` + + 'defaults and leaving settings untouched for this session.', + e + ); + } else { + if (errCode(e) !== 'ENOENT') { + Logger.warn(`${settingsPath} is unreadable; keeping a copy`, e); + await fs + .copy(settingsPath, `${settingsPath}.corrupt`) + .catch(() => {}); + } + const recovered = await fs + .readJSON(`${settingsPath}.tmp`) + .catch(() => null); + if (recovered && typeof recovered === 'object') { + Logger.warn(`Recovered settings from ${settingsPath}.tmp`); + json = recovered as Record; + } + } + } + + const merged = dropUndefined({ + ...json, + isPortable: !!portableDir, + clientDir: portableDir ?? json.clientDir + }); + + const parsed = PreferencesSchema.safeParse(merged); + if (parsed.success) + return this.#withKnownClientDir( + this.#withFreshInstallDefaults(parsed.data) + ); + + Logger.warn( + 'settings.json failed validation; salvaging valid fields', + parsed.error + ); + await fs.copy(settingsPath, `${settingsPath}.corrupt`).catch(() => {}); + + const salvaged: Record = dropUndefined({ + isPortable: !!portableDir, + // coerce to string/undefined; the shape loop never clears a set key, so a + // non-string would survive and throw at the final parse + clientDir: + portableDir ?? + (typeof json.clientDir === 'string' ? json.clientDir : undefined) + }); + const shape = PreferencesSchema.shape; + for (const key of Object.keys(shape) as (keyof typeof shape)[]) { + if (!(key in merged)) continue; + const value = (merged as Record)[key]; + if (shape[key].safeParse(value).success) salvaged[key] = value; + } + // defaults if even the salvaged set is invalid; never throw out of load() + const salvagedParsed = PreferencesSchema.safeParse(salvaged); + return this.#withKnownClientDir( + salvagedParsed.success ? salvagedParsed.data : PreferencesSchema.parse({}) + ); + } + + static async #withKnownClientDir(data: PreferencesSchema) { + if (portableDir) return data; + + const remembered = await fs + .readJSON(this.#installPath) + .then(j => { + const dir = (j as { clientDir?: unknown })?.clientDir; + return typeof dir === 'string' && dir ? dir : undefined; + }) + .catch(() => undefined); + this.#rememberedClientDir = remembered; + + if (await this.isValidClientDir(data.clientDir)) return data; + if (!remembered || remembered === data.clientDir) return data; + if (!(await this.isValidClientDir(remembered))) return data; + + Logger.warn( + `No usable clientDir in settings.json; restored "${remembered}" from ` + + this.#installPath + ); + return { ...data, clientDir: remembered }; + } + + static get data(): PreferencesSchema { + return this.#data; + } + + static set data(newData: Partial>) { + this.#data = { ...this.#data, ...newData }; + + if (this.#readOnly) return; + + const settingsPath = this.#settingsPath; + const dropped = portableDir ? ['isPortable', 'clientDir'] : ['isPortable']; + const delta = dropUndefined( + omit(newData, dropped as (keyof typeof newData)[]) + ); + const snapshot = dropUndefined( + omit(this.#data, dropped as (keyof PreferencesSchema)[]) + ); + this.#writeChain = this.#writeChain + .then(async () => { + let base: Record | null = null; + try { + const onDisk = await readJsonRetrying(settingsPath); + base = + !!onDisk && typeof onDisk === 'object' && !Array.isArray(onDisk) + ? (onDisk as Record) + : null; + } catch (e) { + if (isLocked(e)) { + Logger.error( + `Skipping settings write; ${settingsPath} is locked (${errCode( + e + )})`, + e + ); + return; + } + } + const merged = base ? { ...base, ...delta } : snapshot; + await writeJsonAtomic(settingsPath, merged); + + const clientDir = (merged as { clientDir?: unknown }).clientDir; + if ( + typeof clientDir === 'string' && + clientDir && + clientDir !== this.#rememberedClientDir + ) { + this.#rememberedClientDir = clientDir; + await writeJsonAtomic(this.#installPath, { clientDir }).catch(e => + Logger.warn(`Failed to write ${this.#installPath}`, e) + ); + } + }) + .catch(e => Logger.error('Failed to persist settings.json', e)); + } + + static save() { + return this.#writeChain; + } + + static async isValidClientDir(clientDir?: string) { + if (!clientDir) return false; + if (await fs.exists(path.join(clientDir, 'WoW.exe'))) return true; + // torrent mode: no WoW.exe yet, accept a dir the download can populate + if (isTorrentMode()) + return ( + (await fs.exists(clientDir)) || + (await fs.exists(path.dirname(clientDir))) + ); + return false; + } +} + +export default Preferences; diff --git a/src/main/modules/selfUpdater.ts b/src/main/modules/selfUpdater.ts new file mode 100644 index 0000000..d715a2c --- /dev/null +++ b/src/main/modules/selfUpdater.ts @@ -0,0 +1,122 @@ +import { app } from 'electron'; +import { autoUpdater } from 'electron-updater'; +import Logger from 'electron-log/main'; +import { is } from '@electron-toolkit/utils'; + +import Observable from './observable'; + +export type SelfUpdaterStatus = + | { state: 'idle'; currentVersion: string } + | { state: 'checking'; currentVersion: string } + | { state: 'unavailable'; currentVersion: string } + | { state: 'available'; currentVersion: string; nextVersion: string } + | { + state: 'downloading'; + currentVersion: string; + nextVersion: string; + progress: number; + } + | { state: 'ready'; currentVersion: string; nextVersion: string } + | { state: 'error'; currentVersion: string; message: string }; + +class SelfUpdaterClass extends Observable { + protected _value: SelfUpdaterStatus = { + state: 'idle', + currentVersion: app.getVersion() + }; + + #initialized = false; + #nextVersion: string | undefined; + + get status(): SelfUpdaterStatus { + return this._value; + } + + private set status(v: SelfUpdaterStatus) { + this._value = v; + this._notifyObservers(); + } + + init() { + if (this.#initialized) return; + this.#initialized = true; + + if (is.dev) { + Logger.info('[selfUpdater] dev mode, skipping'); + return; + } + + const currentVersion = app.getVersion(); + + autoUpdater.logger = Logger; + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = false; + + autoUpdater.on('checking-for-update', () => { + Logger.info('[selfUpdater] checking'); + this.status = { state: 'checking', currentVersion }; + }); + autoUpdater.on('update-available', info => { + Logger.info(`[selfUpdater] update available: ${info.version}`); + this.#nextVersion = info.version; + this.status = { + state: 'available', + currentVersion, + nextVersion: info.version + }; + }); + autoUpdater.on('update-not-available', info => { + Logger.info(`[selfUpdater] up to date (current: ${info.version})`); + this.status = { state: 'unavailable', currentVersion }; + }); + autoUpdater.on('error', err => { + Logger.error('[selfUpdater] error', err); + this.status = { + state: 'error', + currentVersion, + message: err?.message ?? String(err) + }; + }); + autoUpdater.on('download-progress', p => { + Logger.info(`[selfUpdater] downloading ${Math.round(p.percent)}%`); + this.status = { + state: 'downloading', + currentVersion, + nextVersion: this.#nextVersion ?? '', + progress: Math.max(0, Math.min(1, p.percent / 100)) + }; + }); + autoUpdater.on('update-downloaded', info => { + Logger.info( + `[selfUpdater] downloaded ${info.version}, awaiting user click` + ); + this.status = { + state: 'ready', + currentVersion, + nextVersion: info.version + }; + }); + + autoUpdater.checkForUpdates().catch(err => { + Logger.error('[selfUpdater] checkForUpdates failed', err); + }); + } + + triggerInstall() { + if (this._value.state !== 'ready') { + Logger.warn( + `[selfUpdater] triggerInstall called in state ${this._value.state}, ignoring` + ); + return; + } + Logger.info( + '[selfUpdater] user clicked install, quitting + running installer' + ); + autoUpdater.quitAndInstall(false, true); + } +} + +const SelfUpdater = new SelfUpdaterClass(); +export default SelfUpdater; + +export const initSelfUpdater = () => SelfUpdater.init(); diff --git a/src/main/modules/tray.ts b/src/main/modules/tray.ts new file mode 100644 index 0000000..2cb71aa --- /dev/null +++ b/src/main/modules/tray.ts @@ -0,0 +1,53 @@ +import { Tray, Menu, nativeImage, app } from 'electron'; +import Logger from 'electron-log/main'; + +import icon from '~build/icon.png?asset'; + +import { mainWindow } from '~main/index'; + +let tray: Tray | null = null; +let isMinimizedToTray = false; + +const restoreWindow = () => { + if (!mainWindow) return; + mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + isMinimizedToTray = false; +}; + +const ensureTray = () => { + if (tray) return tray; + const trayIcon = nativeImage.createFromPath(icon).resize({ width: 16, height: 16 }); + tray = new Tray(trayIcon); + tray.setToolTip('OctoLauncher'); + tray.setContextMenu( + Menu.buildFromTemplate([ + { label: 'Show launcher', click: restoreWindow }, + { type: 'separator' }, + { label: 'Quit', click: () => app.quit() } + ]) + ); + tray.on('click', restoreWindow); + return tray; +}; + +export const minimizeToTray = () => { + if (!mainWindow) return; + ensureTray(); + mainWindow.hide(); + isMinimizedToTray = true; + Logger.info('Minimized to tray'); +}; + +export const restoreFromTray = () => { + if (!isMinimizedToTray) return; + restoreWindow(); +}; + +export const isInTray = () => isMinimizedToTray; + +export const destroyTray = () => { + tray?.destroy(); + tray = null; +}; diff --git a/src/main/modules/updater.ts b/src/main/modules/updater.ts new file mode 100644 index 0000000..f67c088 --- /dev/null +++ b/src/main/modules/updater.ts @@ -0,0 +1,542 @@ +import path from 'node:path'; +import crypto from 'node:crypto'; +import { exec } from 'node:child_process'; +import os from 'node:os'; + +import { app } from 'electron'; +import fetch from 'node-fetch'; +import fs from 'fs-extra'; +import Logger from 'electron-log/main'; + +import { nestedGet, nestedSet } from '~common/utils'; +import { mainWindow } from '~main/index'; +import { getClientVersion } from '~main/utils'; +import { + torrentUrl, + fetchTorrentSha, + syncClient, + refreshPristineWow, + clearTorrentResumeState, + raidVisualsUrl, + clientPatchUrl, + pruneStaleArchives, + torrentTreeIntact, + torrentDownloadSelection, + startSeeding, + stopSeeding +} from '~main/modules/aria2'; + +import Preferences from './preferences'; +import Observable from './observable'; + +type FolderTags = 'allowExtra'; +type FileTags = 'vanillaFixes' | 'raidVisuals'; +type FileManifest = { name: string } & ( + | { type: 'del' } + | { type: 'dir'; files: FileManifest[]; tags?: FolderTags[] } + | { type: 'mpq'; files: FileManifest[]; hash: string; size: number } + | { + type: 'file'; + hash: string; + version?: number; + size: number; + tags?: FileTags[]; + } +); + +type CacheEntry = [hash: string, mtime: number]; +type CacheTree = { [key: string]: CacheTree & CacheEntry }; + +const getManifestItem = ( + m?: FileManifest, + p?: string[] +): FileManifest | undefined => { + if (!p?.length) return m; + + if (m?.type === 'file' || m?.type === 'del') + throw Error(`Can't access ${p.join('.')} from file ${m.name}`); + + const [next, ...rest] = p; + return getManifestItem( + m?.files.find(f => f.name === next), + rest + ); +}; + +const ownedDataArchives = async (): Promise> => { + try { + const j = await fs.readJSON( + path.join(Preferences.userDataDir, 'manifest.json') + ); + const data = getManifestItem(j?.root ?? j, ['Data']); + if (!data || data.type === 'file' || data.type === 'del') return new Set(); + return new Set( + data.files + .filter(f => f.type !== 'dir' && /\.mpq$/i.test(f.name)) + .map(f => f.name.toLowerCase()) + ); + } catch { + return new Set(); + } +}; + +export const isGameRunning = (executablePath: string) => + os.platform() === 'win32' + ? new Promise(resolve => { + const exeName = path.basename(executablePath); + exec( + `tasklist /FI "IMAGENAME eq ${exeName}" /FO CSV /NH`, + (error, stdout) => { + if (error) { + Logger.warn( + `tasklist probe for "${exeName}" failed; assuming game ` + + `is not running. Error: ${error.message}` + ); + resolve(false); + return; + } + resolve( + stdout.toLowerCase().includes(`"${exeName.toLowerCase()}"`) + ); + } + ); + }) + : false; + +type UpdaterState = + | 'verifying' + | 'serverUnreachable' + | 'noClient' + | 'updateAvailable' + | 'updating' + | 'upToDate' + | 'failed'; + +export type UpdaterStatus = { + state: UpdaterState; + progress?: number; + message?: string; + bytesDone?: number; + bytesTotal?: number; + bytesPerSecond?: number; + etaSeconds?: number; +}; + +const SIDECAR_TIMEOUT_MS = 30_000; + +class UpdaterClass extends Observable { + #cachePath = path.join(Preferences.userDataDir, 'cache.json'); + #cache: CacheTree = this.#readCache(); + + #readCache(): CacheTree { + try { + return fs.existsSync(this.#cachePath) + ? fs.readJSONSync(this.#cachePath) + : {}; + } catch { + return {}; + } + } + + async #saveCache() { + await fs.writeJSON(this.#cachePath, this.#cache); + } + + async #getHash(clientPath: string, ...filePath: string[]) { + if (!(await fs.exists(path.join(clientPath, ...filePath)))) { + nestedSet(this.#cache, filePath, undefined); + return undefined; + } + + const stats = await fs.stat(path.join(clientPath, ...filePath)); + if (stats.isDirectory()) + throw Error(`Tried to get hash of directory ${path.join(...filePath)}`); + + const c = nestedGet(this.#cache, filePath); + + if (c?.[0] && c[1] === stats.mtimeMs) return c[0]; + + const newHash = crypto + .createHash('sha1') + .update(await fs.readFile(path.join(clientPath, ...filePath))) + .digest('hex') + .toLocaleUpperCase(); + nestedSet(this.#cache, filePath, { + ...c, + [0]: newHash, + [1]: stats.mtimeMs + }); + return newHash; + } + + protected _value: UpdaterStatus = { state: 'failed' }; + + get status() { + return this._value; + } + private set status(v: UpdaterStatus) { + this._value = v; + this._notifyObservers(v); + if (this.status.state === 'failed') { + mainWindow?.setProgressBar(1, { mode: 'error' }); + } else if (this.status.progress === 1) { + mainWindow?.setProgressBar(0); + } else { + mainWindow?.setProgressBar(this.status.progress ?? 0, { + mode: this.status.progress === -1 ? 'indeterminate' : 'normal' + }); + } + } + + async refreshSeeding() { + if ( + Preferences.data.shareDownloads !== false && + Preferences.data.clientDir && + this.status.state === 'upToDate' + ) + await startSeeding(Preferences.data.clientDir); + else stopSeeding(); + } + + async #torrentVerify(clientPath: string) { + const url = torrentUrl(); + if (!url) { + this.status = { state: 'serverUnreachable' }; + return; + } + this.status = { + state: 'verifying', + progress: -1, + message: 'Checking for updates...' + }; + try { + const sha = await fetchTorrentSha(url); + await this.#reconcileClientPatch(clientPath); + await this.#reconcileRaidVisuals(clientPath); + const haveExe = await fs.pathExists(path.join(clientPath, 'WoW.exe')); + if ( + haveExe && + sha !== Preferences.data.syncedTorrentHash && + (await this.#torrentFastForward(clientPath, sha, url)) + ) { + this.status = { state: 'upToDate', progress: 1 }; + await this.refreshSeeding(); + return; + } + const upToDate = + haveExe && + sha === Preferences.data.syncedTorrentHash && + (await torrentTreeIntact(clientPath, url)); + if (upToDate) { + const removed = await pruneStaleArchives(clientPath, url, new Set()); + if (removed.length) + Logger.log(`Removed stale archives: ${removed.join(', ')}`); + } + this.status = upToDate + ? { state: 'upToDate', progress: 1 } + : { state: 'updateAvailable' }; + await this.refreshSeeding(); + } catch (e) { + Logger.error('Torrent verify failed', e); + this.status = { state: 'serverUnreachable' }; + } + } + + async #fetchSidecarFile( + url: string, + dest: string, + storedHash: string | undefined, + busyMessage: string + ): Promise { + const ac = new AbortController(); + const t = setTimeout(() => ac.abort(), SIDECAR_TIMEOUT_MS); + const sha = await fetch(`${url}.sha256`, { signal: ac.signal }) + .then(r => { + if (!r.ok) throw new Error(`sha256 HTTP ${r.status}`); + return r.text(); + }) + .then(s => s.trim()) + .finally(() => clearTimeout(t)); + if ((await fs.pathExists(dest)) && storedHash === sha) return undefined; + this.status = { state: 'updating', progress: -1, message: busyMessage }; + const buf = await this.#downloadWithIdleTimeout(url); + if (crypto.createHash('sha256').update(buf).digest('hex') !== sha) + throw new Error('checksum mismatch'); + await fs.ensureDir(path.dirname(dest)); + await fs.writeFile(`${dest}.part`, buf); + await fs.move(`${dest}.part`, dest, { overwrite: true }); + return sha; + } + + async #downloadWithIdleTimeout(url: string): Promise { + const ac = new AbortController(); + let idle: ReturnType | undefined; + const arm = () => { + if (idle) clearTimeout(idle); + idle = setTimeout(() => ac.abort(), SIDECAR_TIMEOUT_MS); + }; + arm(); + try { + const res = await fetch(url, { signal: ac.signal }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const body = res.body as NodeJS.ReadableStream | null; + if (!body) throw new Error('empty response body'); + const chunks: Buffer[] = []; + for await (const chunk of body) { + arm(); + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); + } finally { + if (idle) clearTimeout(idle); + } + } + + async #reconcileClientPatch(clientPath: string) { + const url = clientPatchUrl(); + if (!url) return; + const dest = path.join(clientPath, 'Data', 'patch-5.mpq'); + try { + const sha = await this.#fetchSidecarFile( + url, + dest, + Preferences.data.clientPatchHash, + 'Updating game files...' + ); + if (sha) Preferences.data = { clientPatchHash: sha }; + } catch (e) { + Logger.warn('Content patch reconcile failed', e); + if (!(await fs.pathExists(dest))) + throw new Error( + 'Could not download core game content. Please check your connection and try again.' + ); + } + } + + async #reconcileRaidVisuals(clientPath: string) { + const url = raidVisualsUrl(); + if (!url) return; + const dest = path.join(clientPath, 'Data', 'patch-O.mpq'); + try { + if (!Preferences.data.config?.raidVisuals) { + if (await fs.pathExists(dest)) { + await fs.remove(dest); + Preferences.data = { raidVisualsHash: undefined }; + } + return; + } + const sha = await this.#fetchSidecarFile( + url, + dest, + Preferences.data.raidVisualsHash, + 'Updating raid visuals...' + ); + if (sha) Preferences.data = { raidVisualsHash: sha }; + } catch (e) { + Logger.warn('Raid visuals reconcile failed', e); + } + } + + async syncRaidVisuals() { + if (this.status?.state === 'verifying' || this.status?.state === 'updating') + return; + const clientPath = Preferences.data.clientDir; + if (!clientPath) return; + if (await isGameRunning(path.join(clientPath, 'WoW.exe'))) { + this.status = { + state: 'failed', + message: 'Please close WoW first, before updating.' + }; + return; + } + if (!torrentUrl()) return this.verify(); + await this.#reconcileRaidVisuals(clientPath); + this.status = { state: 'upToDate', progress: 1 }; + } + + async #torrentFastForward( + clientPath: string, + sha: string, + url: string + ): Promise { + if (!Preferences.data.syncedTorrentHash) return false; + if (!(await torrentTreeIntact(clientPath, url))) return false; + await refreshPristineWow(clientPath); + const removed = await pruneStaleArchives( + clientPath, + url, + await ownedDataArchives() + ); + if (removed.length) + Logger.log(`Removed stale archives: ${removed.join(', ')}`); + Preferences.data = { + syncedTorrentHash: sha, + version: await getClientVersion() + }; + return true; + } + + async #torrentUpdate(clientPath: string, clean?: boolean) { + const url = torrentUrl(); + if (!url) { + this.status = { state: 'serverUnreachable' }; + return; + } + try { + stopSeeding(); + const sha = await fetchTorrentSha(url); + if (!clean && (await this.#torrentFastForward(clientPath, sha, url))) { + await this.#reconcileClientPatch(clientPath); + await this.#reconcileRaidVisuals(clientPath); + this.status = { state: 'upToDate', progress: 1 }; + await this.refreshSeeding(); + return; + } + const isUpdate = await fs.pathExists(path.join(clientPath, 'WoW.exe')); + const staleContext = + Preferences.data.activeTorrentHash !== sha || + Preferences.data.activeClientDir !== clientPath; + if (staleContext) { + await clearTorrentResumeState(); + Preferences.data = { + activeTorrentHash: sha, + activeClientDir: clientPath + }; + } + const selection = clean + ? null + : await torrentDownloadSelection(clientPath, url, staleContext); + const selectFiles = + selection && selection.length > 0 ? selection : undefined; + this.status = { + state: 'updating', + progress: -1, + message: clean + ? 'Verifying game files...' + : isUpdate + ? 'Connecting...' + : 'Preparing download...' + }; + let downloading = false; + await syncClient({ + torrentUrl: url, + clientDir: clientPath, + checkIntegrity: !!clean, + selectFiles, + seedTime: 0, + onProgress: p => { + if (p.bytesPerSecond > 0) downloading = true; + const phase = downloading + ? 'Downloading' + : clean + ? 'Verifying game files' + : isUpdate + ? 'Connecting' + : 'Preparing'; + this.status = { + state: 'updating', + progress: p.progress, + bytesDone: p.bytesDone, + bytesTotal: p.bytesTotal, + bytesPerSecond: p.bytesPerSecond, + message: `${phase}...` + }; + } + }); + if (!(await torrentTreeIntact(clientPath, url))) { + this.status = { + state: 'updateAvailable', + message: 'Download incomplete. Click update to finish.' + }; + return; + } + await refreshPristineWow(clientPath); + const removed = await pruneStaleArchives( + clientPath, + url, + await ownedDataArchives() + ); + if (removed.length) + Logger.log(`Removed stale archives: ${removed.join(', ')}`); + await this.#reconcileClientPatch(clientPath); + await this.#reconcileRaidVisuals(clientPath); + Preferences.data = { + syncedTorrentHash: sha, + version: await getClientVersion() + }; + this.status = { state: 'upToDate', progress: 1 }; + await this.refreshSeeding(); + } catch (e) { + Logger.error('Torrent update failed', e); + this.status = { + state: 'failed', + message: e instanceof Error ? e.message : 'Download failed' + }; + } + } + + async verify() { + if (this.status?.state === 'verifying' || this.status?.state === 'updating') + return; + + const clientPath = Preferences.data.clientDir; + if (!clientPath) { + this.status = { state: 'noClient' }; + return; + } + + if (os.platform() === 'win32' && clientPath.length > 220) { + this.status = { + state: 'failed', + message: + 'Path to current install location is too long and may cause issues.' + }; + return; + } + + if (await isGameRunning(path.join(clientPath, 'WoW.exe'))) { + this.status = { + state: 'failed', + message: 'Please close WoW first, before updating.' + }; + return; + } + + return this.#torrentVerify(clientPath); + } + + async update(clean?: boolean) { + if (this.status?.state === 'verifying' || this.status?.state === 'updating') + return; + + const clientPath = Preferences.data.clientDir; + if (!clientPath) { + this.status = { state: 'noClient' }; + return; + } + + if (await isGameRunning(path.join(clientPath, 'WoW.exe'))) { + this.status = { + state: 'failed', + message: 'Please close WoW first, before updating.' + }; + return; + } + + return this.#torrentUpdate(clientPath, clean); + } + + async recordPatchedWow() { + const clientPath = Preferences.data.clientDir; + if (!clientPath) return; + const patchedWowHash = await this.#getHash(clientPath, 'WoW.exe'); + await this.#saveCache(); + Preferences.data = { + lastPatchedLauncherVersion: app.getVersion(), + expectedPatchedWowHash: patchedWowHash + }; + } +} + +const Updater = new UpdaterClass(); +export default Updater; diff --git a/src/main/modules/upnp.ts b/src/main/modules/upnp.ts new file mode 100644 index 0000000..ec3abd2 --- /dev/null +++ b/src/main/modules/upnp.ts @@ -0,0 +1,374 @@ +import dgram from 'dgram'; +import http from 'http'; +import os from 'os'; + +import Logger from 'electron-log/main'; + +// UPnP-IGD port mapping (best effort) for a NAT'd seeder; node builtins only, no-ops on failure. + +export type PortMapping = { stop: () => Promise }; + +const NOOP: PortMapping = { stop: async () => {} }; + +const SSDP_ADDR = '239.255.255.250'; +const SSDP_PORT = 1900; + +const SEARCH = Buffer.from( + [ + 'M-SEARCH * HTTP/1.1', + `HOST: ${SSDP_ADDR}:${SSDP_PORT}`, + 'MAN: "ssdp:discover"', + 'MX: 2', + 'ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1', + '', + '' + ].join('\r\n') +); + +// exposes AddPortMapping, newest first +const WAN_SERVICES = [ + 'urn:schemas-upnp-org:service:WANIPConnection:2', + 'urn:schemas-upnp-org:service:WANIPConnection:1', + 'urn:schemas-upnp-org:service:WANPPPConnection:1' +]; + +type Gateway = { location: string; address: string; localAddress: string }; +type WanService = { controlUrl: string; serviceType: string }; + +class SoapError extends Error { + code?: string; + constructor(message: string, code?: string) { + super(message); + this.code = code; + } +} + +const candidateAddresses = (): string[] => + Object.values(os.networkInterfaces()) + .flat() + .filter( + (a): a is os.NetworkInterfaceInfo => + !!a && + a.family === 'IPv4' && + !a.internal && + !a.address.startsWith('169.254.') + ) + .map(a => a.address); + +// a 0.0.0.0/empty host in LOCATION is really the address the datagram came from +const fixLocation = (location: string, responder: string): string => { + try { + const u = new URL(location); + if (u.hostname === '0.0.0.0' || u.hostname === '') u.hostname = responder; + return u.toString(); + } catch { + return location; + } +}; + +// M-SEARCH one interface; collect every responder (more than one can answer) +const searchInterface = ( + localAddress: string, + timeoutMs: number +): Promise => + new Promise(resolve => { + const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true }); + const found = new Map(); + let retry: ReturnType | undefined; + let done = false; + const finish = () => { + if (done) return; + done = true; + if (retry) clearInterval(retry); + try { + socket.close(); + } catch { + // already closed + } + resolve([...found.values()]); + }; + socket.on('message', (msg, rinfo) => { + const m = /^location:\s*(\S+)/im.exec(msg.toString('utf8')); + if (!m) return; + const location = fixLocation(m[1].trim(), rinfo.address); + if (!found.has(location)) + found.set(location, { location, address: rinfo.address, localAddress }); + }); + socket.on('error', () => finish()); + socket.bind(0, localAddress, () => { + try { + socket.setMulticastInterface(localAddress); + } catch { + // fall back to the default multicast interface + } + const send = () => + socket.send(SEARCH, SSDP_PORT, SSDP_ADDR, () => { + /* fire-and-forget */ + }); + send(); + // Routers sometimes miss the first datagram; re-ask until the window closes. + retry = setInterval(send, 700); + setTimeout(finish, timeoutMs); + }); + }); + +// search all interfaces: a VPN often owns the default route +const discoverGateways = async (timeoutMs: number): Promise => { + const perInterface = await Promise.all( + candidateAddresses().map(a => searchInterface(a, timeoutMs)) + ); + const seen = new Set(); + const gateways: Gateway[] = []; + for (const list of perInterface) + for (const gw of list) + if (!seen.has(gw.location)) { + seen.add(gw.location); + gateways.push(gw); + } + return gateways; +}; + +// build the control URL from the host we reached; routers advertise a bogus URLBase +const controlUrlFrom = (descriptorUrl: string, controlPath: string): string => { + const desc = new URL(descriptorUrl); + let path: string; + try { + const c = new URL(controlPath, descriptorUrl); + path = `${c.pathname}${c.search}`; + } catch { + path = controlPath.startsWith('/') ? controlPath : `/${controlPath}`; + } + return `${desc.protocol}//${desc.host}${path}`; +}; + +// raw http, not fetch: many UPnP servers are non-compliant and undici rejects them +const httpRequest = ( + url: string, + opts: { + method?: string; + headers?: Record; + body?: string; + timeoutMs?: number; + } = {} +): Promise<{ status: number; body: string }> => + new Promise((resolve, reject) => { + let u: URL; + try { + u = new URL(url); + } catch (e) { + reject(e as Error); + return; + } + const headers = { ...(opts.headers ?? {}) }; + const body = opts.body ? Buffer.from(opts.body, 'utf8') : undefined; + if (body) headers['Content-Length'] = String(body.length); + const req = http.request( + { + hostname: u.hostname, + port: u.port || 80, + path: `${u.pathname}${u.search}`, + method: opts.method ?? 'GET', + headers + }, + res => { + const chunks: Buffer[] = []; + res.on('data', c => chunks.push(c)); + res.on('end', () => + resolve({ + status: res.statusCode ?? 0, + body: Buffer.concat(chunks).toString('utf8') + }) + ); + } + ); + req.on('error', reject); + req.setTimeout(opts.timeoutMs ?? 5000, () => + req.destroy(new Error('request timed out')) + ); + if (body) req.write(body); + req.end(); + }); + +// first WAN service + control URL from a device descriptor +const findWanService = ( + xml: string, + descriptorUrl: string +): WanService | undefined => { + for (const block of xml.split(//i).slice(1)) { + const type = /\s*([^<]+?)\s*<\/serviceType>/i + .exec(block)?.[1] + ?.trim(); + const ctrl = /\s*([^<]+?)\s*<\/controlURL>/i + .exec(block)?.[1] + ?.trim(); + if ( + type && + ctrl && + WAN_SERVICES.some(w => w.toLowerCase() === type.toLowerCase()) + ) + return { + controlUrl: controlUrlFrom(descriptorUrl, ctrl), + serviceType: type + }; + } + return undefined; +}; + +const xmlEscape = (s: string): string => + s.replace( + /[<>&'"]/g, + c => + ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' }[ + c + ] as string) + ); + +const arg = (name: string, value: string | number): string => + `<${name}>${value}`; + +const soap = async ( + svc: WanService, + action: string, + body: string +): Promise => { + const envelope = + '' + + '' + + '' + + `${body}` + + ''; + const res = await httpRequest(svc.controlUrl, { + method: 'POST', + headers: { + 'Content-Type': 'text/xml; charset="utf-8"', + 'SOAPAction': `"${svc.serviceType}#${action}"` + }, + body: envelope + }); + if (res.status < 200 || res.status >= 300) { + const code = /\s*(\d+)/i.exec(res.body)?.[1]; + throw new SoapError( + `${action} failed: HTTP ${res.status}${code ? ` (UPnP ${code})` : ''}`, + code + ); + } +}; + +const addMapping = ( + svc: WanService, + port: number, + protocol: 'TCP' | 'UDP', + client: string, + description: string, + lease: number +): Promise => + soap( + svc, + 'AddPortMapping', + arg('NewRemoteHost', '') + + arg('NewExternalPort', port) + + arg('NewProtocol', protocol) + + arg('NewInternalPort', port) + + arg('NewInternalClient', client) + + arg('NewEnabled', 1) + + arg('NewPortMappingDescription', xmlEscape(description)) + + arg('NewLeaseDuration', lease) + ); + +const deleteMapping = ( + svc: WanService, + port: number, + protocol: 'TCP' | 'UDP' +): Promise => + soap( + svc, + 'DeletePortMapping', + arg('NewRemoteHost', '') + + arg('NewExternalPort', port) + + arg('NewProtocol', protocol) + ); + +// map port (TCP+UDP), kept alive until stop(); returns a no-op handle when no gateway +export const mapPort = async ( + port: number, + opts: { description?: string; ttlSeconds?: number } = {} +): Promise => { + const description = opts.description ?? 'OctoWoW'; + try { + const gateways = await discoverGateways(4000); + if (!gateways.length) { + Logger.log('UPnP: no gateway found; seeding without a port mapping'); + return NOOP; + } + // take the first responder that exposes a WAN service + const probed = await Promise.all( + gateways.map(async gw => { + const res = await httpRequest(gw.location).catch(() => undefined); + const svc = + res && res.status < 400 + ? findWanService(res.body, gw.location) + : undefined; + return svc ? { svc, client: gw.localAddress } : undefined; + }) + ); + const target = probed.find(Boolean); + if (!target) { + Logger.log('UPnP: no gateway exposes a WAN service; skipping mapping'); + return NOOP; + } + const { svc, client } = target; + + // Some routers only grant permanent leases (UPnP error 725); fall back to one. + let lease = opts.ttlSeconds ?? 3600; + const mapped: ('TCP' | 'UDP')[] = []; + const mapOne = async (protocol: 'TCP' | 'UDP') => { + try { + await addMapping(svc, port, protocol, client, description, lease); + } catch (e) { + if (e instanceof SoapError && e.code === '725' && lease !== 0) { + lease = 0; + await addMapping(svc, port, protocol, client, description, lease); + } else throw e; + } + mapped.push(protocol); + }; + try { + await mapOne('TCP'); + await mapOne('UDP'); + } catch (e) { + for (const p of mapped) await deleteMapping(svc, port, p).catch(() => {}); + throw e; + } + + // A finite lease self-heals if we exit uncleanly; renew ahead of expiry. + let renew: ReturnType | undefined; + if (lease > 0) { + const period = Math.max(60_000, (lease - 60) * 1000); + renew = setInterval(() => { + addMapping(svc, port, 'TCP', client, description, lease).catch( + () => {} + ); + addMapping(svc, port, 'UDP', client, description, lease).catch( + () => {} + ); + }, period); + renew.unref?.(); + } + + Logger.log( + `UPnP: mapped ${port} TCP+UDP to ${client} (lease ${ + lease || 'permanent' + })` + ); + return { + stop: async () => { + if (renew) clearInterval(renew); + await deleteMapping(svc, port, 'TCP').catch(() => {}); + await deleteMapping(svc, port, 'UDP').catch(() => {}); + } + }; + } catch (e) { + Logger.warn('UPnP: port mapping failed; seeding without it', e); + return NOOP; + } +}; diff --git a/src/main/types.d.ts b/src/main/types.d.ts new file mode 100644 index 0000000..9ef4899 --- /dev/null +++ b/src/main/types.d.ts @@ -0,0 +1,13 @@ +export { type AppRouter } from './api/root'; +export { type UpdaterStatus } from './modules/updater'; +export { type AddonsStatus, type AddonData } from './modules/addons'; +export { + type ModsStatus, + type ModRowStatus, + type CustomMod +} from './modules/mods'; +export { + type NewsItem, + type NewsFeed, + type ForumAnnouncement +} from '../common/schemas'; diff --git a/src/main/utils.ts b/src/main/utils.ts new file mode 100644 index 0000000..eab9a5c --- /dev/null +++ b/src/main/utils.ts @@ -0,0 +1,69 @@ +import { type Worker, type WorkerOptions } from 'node:worker_threads'; +import path from 'node:path'; + +import Logger from 'electron-log/main'; +import fs from 'fs-extra'; + +import Preferences from './modules/preferences'; + +const isCallbackResponse = ( + data: unknown +): data is { cb: string; args: unknown[] } => + typeof data === 'object' && + data !== null && + 'cb' in data && + typeof (data as { cb: unknown }).cb === 'string' && + 'args' in data && + Array.isArray((data as { args: unknown }).args); + +export const runWorker = ( + worker: (o: WorkerOptions) => Worker, + workerData: Record, + callbacks?: Record void> +) => + new Promise((resolve, reject) => + worker({ workerData }) + .on('message', (m: unknown) => { + if (!isCallbackResponse(m)) return resolve(m as T); + const callback = callbacks?.[m.cb]; + if (callback) callback(...m.args); + else Logger.warn('Unknown worker callback', m.cb); + }) + .on('error', reject) + .on('exit', code => + reject(new Error(`Worker exited (code ${code}) without finishing`)) + ) + ); + +export const getClientVersion = async () => { + Logger.log('Reading client version...'); + + const exePath = path.join(Preferences.data.clientDir ?? '', 'WoW.exe'); + + if (!(await fs.exists(exePath))) { + Logger.log('Client not found...'); + return undefined; + } + + const file = await fs.readFile(exePath); + const buffer = Buffer.from(file); + + const VERSION_OFFSET = 0x00437c04; + const VERSION_LEN = 6; + const BUILD_OFFSET = 0x00437bfc; + const BUILD_LEN = 4; + + const version = buffer.toString( + 'utf-8', + VERSION_OFFSET, + VERSION_OFFSET + VERSION_LEN + ); + const build = buffer.toString( + 'utf-8', + BUILD_OFFSET, + BUILD_OFFSET + BUILD_LEN + ); + + Logger.log(`Client version is: ${version} (${build})`); + return `${version} (${build})`; +}; diff --git a/src/main/workers/gitClone.ts b/src/main/workers/gitClone.ts new file mode 100644 index 0000000..42ae74f --- /dev/null +++ b/src/main/workers/gitClone.ts @@ -0,0 +1,46 @@ +import { workerData, parentPort } from 'worker_threads'; + +import git from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import fs from 'fs-extra'; + +const port = parentPort; +if (!port) throw new Error('IllegalState'); + +const { dir, url, ref } = workerData; + +const tmpDir = `${dir}.tmp`; +const bakDir = `${dir}.bak`; + +const run = async () => { + await fs.remove(tmpDir); + await git.clone({ + dir: tmpDir, + fs, + http, + url, + ref, + singleBranch: !ref || ref === 'master' || ref === 'main', + onProgress: (...args) => port.postMessage({ cb: 'onProgress', args }) + }); + + await fs.remove(bakDir); + const hadExisting = await fs.pathExists(dir); + if (hadExisting) await fs.move(dir, bakDir); + + try { + await fs.move(tmpDir, dir); + } catch (e) { + if (hadExisting) await fs.move(bakDir, dir).catch(() => undefined); + throw e; + } + + await fs.remove(bakDir).catch(() => undefined); +}; + +run() + .then(() => port.postMessage(true)) + .catch(async err => { + await fs.remove(tmpDir).catch(() => undefined); + throw err; + }); diff --git a/src/main/workers/gitPull.ts b/src/main/workers/gitPull.ts new file mode 100644 index 0000000..431f1fa --- /dev/null +++ b/src/main/workers/gitPull.ts @@ -0,0 +1,56 @@ +import { workerData, parentPort } from 'worker_threads'; + +import git from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import fs from 'fs-extra'; + +const port = parentPort; +if (!port) throw new Error('gitPull worker has no parentPort'); + +const { dir, remote, branch, ref } = workerData as { + dir: string; + remote: string; + branch: string; + ref?: string; +}; + +const onProgress = (...args: unknown[]) => + port.postMessage({ cb: 'onProgress', args }); + +const run = async () => { + if (ref) { + await git.fetch({ + fs, + http, + dir, + tags: true, + singleBranch: false, + onProgress + }); + await git.checkout({ fs, dir, force: true, ref, onProgress }); + return; + } + + await git.checkout({ + fs, + dir, + force: true, + ref: `${remote}/${branch}`, + onProgress + }); + await git.pull({ + fs, + http, + dir, + ref: branch, + singleBranch: true, + author: { name: 'Octo Launcher' }, + onProgress + }); +}; + +run() + .then(() => port.postMessage(true)) + .catch(err => { + throw err; + }); diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..375e721 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,16 @@ +import path from 'path'; + +import { contextBridge } from 'electron'; +import { electronAPI } from '@electron-toolkit/preload'; +import { exposeElectronTRPC } from 'electron-trpc/main'; + +try { + contextBridge.exposeInMainWorld('electron', electronAPI); + contextBridge.exposeInMainWorld('path', path); +} catch (error) { + console.error(error); +} + +process.once('loaded', async () => { + exposeElectronTRPC(); +}); diff --git a/src/preload/window.d.ts b/src/preload/window.d.ts new file mode 100644 index 0000000..994e3c4 --- /dev/null +++ b/src/preload/window.d.ts @@ -0,0 +1,10 @@ +import type path from 'path'; + +import { type ElectronAPI } from '@electron-toolkit/preload'; + +declare global { + interface Window { + electron: ElectronAPI; + path: typeof path; + } +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx new file mode 100644 index 0000000..6244300 --- /dev/null +++ b/src/renderer/App.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react'; + +import { api } from './utils/api'; +import PageBackground from './assets/background.png'; +import AntivirusModal from './components/AntivirusModal'; +import Header from './components/Header'; +import LaunchPanel from './components/LaunchPanel'; +import SelfUpdateBanner from './components/SelfUpdateBanner'; +import TabsPanel, { type TabType } from './components/TabsPanel'; +import TopBar from './components/TopBar'; +import IconSpinner from './components/styled/IconSpinner'; +import usePreventDefaultEvents from './utils/usePreventDefaultEvents'; + +const App = () => { + const { isLoading } = api.preferences.get.useQuery(); + const { data: appVersion } = api.general.appVersion.useQuery(); + + const [activeTab, setActiveTab] = useState(); + + usePreventDefaultEvents(); + + return ( +
+ + +
+ + {isLoading ? ( +
+ +
+ ) : ( + <> + + + + )} + + {appVersion && ( + + v{appVersion} + + )} + + +
+ ); +}; + +export default App; diff --git a/src/renderer/ErrorBoundary.tsx b/src/renderer/ErrorBoundary.tsx new file mode 100644 index 0000000..b10bec8 --- /dev/null +++ b/src/renderer/ErrorBoundary.tsx @@ -0,0 +1,91 @@ +import { Clipboard, RefreshCw, ServerCrash } from 'lucide-react'; +import { Component, type ErrorInfo, type ReactNode } from 'react'; +import log from 'electron-log/renderer'; + +import { useT } from '~renderer/i18n'; + +import PageBackground from './assets/background.png'; +import TextButton from './components/styled/TextButton'; + +type State = { + didCatch?: boolean; + error?: Error; + errorInfo?: ErrorInfo; +}; + +type Props = { + children: ReactNode; +}; + +const ErrorFallback = ({ + error, + errorInfo +}: { + error?: Error; + errorInfo?: ErrorInfo; +}) => { + const t = useT(); + const title = t('misc.uncaughtError', { + name: error?.name ?? '', + message: error?.message ?? t('misc.unknownError') + }); + const detail = errorInfo?.componentStack?.slice(1); + return ( +
+
+
+ +

{t('misc.somethingWentWrong')}

+
+
+
{title}
+
+					{detail}
+				
+
+
+ + navigator.clipboard.writeText( + `\`\`\`\n${title}\n${detail}\n\`\`\`` + ) + } + > + {t('misc.copyError')} + + window.location.reload()} + className="text-warmGreen" + > + {t('misc.reload')} + +
+
+
+ ); +}; + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = {}; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + log.error('Client crash:', error, errorInfo); + this.setState({ didCatch: true, error, errorInfo }); + } + + render() { + if (!this.state.didCatch) return this.props.children; + const { error, errorInfo } = this.state; + return ; + } +} + +export default ErrorBoundary; diff --git a/src/renderer/assets/DINPro-Regular.otf b/src/renderer/assets/DINPro-Regular.otf new file mode 100644 index 0000000..84d57ab Binary files /dev/null and b/src/renderer/assets/DINPro-Regular.otf differ diff --git a/src/renderer/assets/FontinSans-Regular.otf b/src/renderer/assets/FontinSans-Regular.otf new file mode 100644 index 0000000..5fde268 Binary files /dev/null and b/src/renderer/assets/FontinSans-Regular.otf differ diff --git a/src/renderer/assets/background.png b/src/renderer/assets/background.png new file mode 100644 index 0000000..25cbdfb Binary files /dev/null and b/src/renderer/assets/background.png differ diff --git a/src/renderer/assets/logo.png b/src/renderer/assets/logo.png new file mode 100644 index 0000000..27aaf04 Binary files /dev/null and b/src/renderer/assets/logo.png differ diff --git a/src/renderer/assets/parchment.jpg b/src/renderer/assets/parchment.jpg new file mode 100644 index 0000000..5164ce7 Binary files /dev/null and b/src/renderer/assets/parchment.jpg differ diff --git a/src/renderer/components/AntivirusModal.tsx b/src/renderer/components/AntivirusModal.tsx new file mode 100644 index 0000000..d453dff --- /dev/null +++ b/src/renderer/components/AntivirusModal.tsx @@ -0,0 +1,230 @@ +import { useEffect, useRef, useState } from 'react'; +import { ShieldAlert, HelpCircle } from 'lucide-react'; +import { createPortal } from 'react-dom'; + +import { api } from '~renderer/utils/api'; +import { type ModsStatus, type UpdaterStatus } from '~main/types'; +import { useT } from '~renderer/i18n'; + +import TextButton from './styled/TextButton'; + +const DETECTION = 'Trojan:Win32/Vigorf.A'; + +const AntivirusModal = () => { + const t = useT(); + const [status, setStatus] = useState(); + api.mods.observe.useSubscription(undefined, { onData: setStatus }); + + // re-scan for AV blocks once the updater settles (not mid-download); catches an aborted + // download or a file quarantined after the fact + const [updateState, setUpdateState] = useState(); + api.updater.observe.useSubscription(undefined, { + onData: s => setUpdateState(s?.state) + }); + const settled = + !!updateState && updateState !== 'verifying' && updateState !== 'updating'; + const { data: quarantined, refetch: refetchQuarantined } = + api.general.antivirusBlocks.useQuery(undefined, { + enabled: false, + refetchOnWindowFocus: false, + staleTime: Infinity + }); + useEffect(() => { + if (settled) refetchQuarantined(); + }, [updateState, settled, refetchQuarantined]); + + const addExclusion = api.general.addDefenderExclusion.useMutation(); + + const blocked = [ + ...new Set([ + ...(quarantined ?? []), + ...(status?.mods ?? []) + .filter(m => m.state === 'error' && m.error?.includes('Defender')) + .map(m => m.name) + ]) + ]; + const blockedKey = blocked.join(','); + + const dialogRef = useRef(null); + const [view, setView] = useState<'av' | 'why' | null>(null); + + useEffect(() => { + if (view) { + // showModal() throws (and crashes to the error screen) if already open, e.g. the av<->why switch + if (!dialogRef.current?.open) dialogRef.current?.showModal(); + (document.activeElement as HTMLElement | null)?.blur(); + } else dialogRef.current?.close(); + }, [view]); + + useEffect(() => { + if (blockedKey) setView('av'); + }, [blockedKey]); + + // The settings dialog opens the explainer straight from its antivirus button. + useEffect(() => { + const open = () => setView('why'); + window.addEventListener('av-help', open); + return () => window.removeEventListener('av-help', open); + }, []); + + const names = blockedKey ? blockedKey.split(',') : []; + + return createPortal( + setView(null)} + className="h-full w-full items-center justify-center bg-[transparent] backdrop:backdrop-blur-sm [&[open]]:flex" + > + {view === 'av' && ( +
+

{t('av.blockedTitle')}

+

+ {names.length === 1 ? t('av.blockedOne') : t('av.blockedMany')} +

+
    + {names.map(n => ( +
  • + • {n} +
  • + ))} +
+

{t('av.blockedExplain')}

+ setView('why')} + className="self-start text-yellow hocus:!text-yellow" + > + {t('av.whyHappening')} + + addExclusion.mutate()} + className="self-start text-orange" + > + {t('av.allowThrough')} + + {addExclusion.data?.ok === true && ( + {t('av.addedRetry')} + )} + {addExclusion.data?.ok === false && ( + {addExclusion.data.error} + )} +
+ setView(null)} className="text-green"> + {t('av.close')} + +
+
+ )} + + {view === 'why' && ( +
+

{t('av.whyTitle')}

+
+

+ {t('av.whyIntro', { detection: DETECTION })}{' '} + {t('av.falsePositive')}. +

+
+

+ {t('av.whatSetsItOff')} +

+

{t('av.whatSetsItOffIntro')}

+
    +
  • + {t('av.dllInjection')}:{' '} + {t('av.dllInjectionText')} +
  • +
  • + {t('av.exePatching')}:{' '} + {t('av.exePatchingText')} +
  • +
+

{t('av.heuristicNote')}

+
+
+

+ {t('av.whatModsDo')} +

+
    +
  • + VanillaFixes:{' '} + {t('av.modVanillaFixes')} +
  • +
  • + nampower:{' '} + {t('av.modNampower')} +
  • +
  • + SuperWoW and UnitXP:{' '} + {t('av.modSuperWowUnitXp')} +
  • +
  • + DXVK:{' '} + {t('av.modDxvk')} +
  • +
+
+
+

+ {t('av.howToVerify')} +

+

+ {t('av.howToVerifyText', { + detection: DETECTION + })} +

+
+
+

+ {t('av.whatAllowDoesTitle')} +

+

+ {t('av.whatAllowDoesIntro')}{' '} + Add-MpPreference{' '} + {t('av.whatAllowDoesIntroAfter')} +

+
    +
  • + {t('av.exclusionFoldersBefore')}{' '} + {t('av.gameFolder')}{' '} + {t('av.exclusionFoldersAnd')}{' '} + + {t('av.launcherFolder')} + + {t('av.exclusionFoldersAfter')} +
  • +
  • + WoW.exe{' '} + {t('av.exclusionExesAnd')}{' '} + VanillaFixes.exe{' '} + {t('av.exclusionExesAfter')} +
  • +
+

+ {t('av.whatAllowDoesOutro')} +

+
+
+
+ {names.length > 0 && ( + setView('av')} + className="text-blueGray" + > + {t('av.back')} + + )} + setView(null)} className="text-green"> + {t('av.close')} + +
+
+ )} +
, + document.body + ); +}; + +export default AntivirusModal; diff --git a/src/renderer/components/ClientDirDialog.tsx b/src/renderer/components/ClientDirDialog.tsx new file mode 100644 index 0000000..9462a9a --- /dev/null +++ b/src/renderer/components/ClientDirDialog.tsx @@ -0,0 +1,148 @@ +import { useForm } from 'react-hook-form'; +import { useEffect, useState } from 'react'; + +import { PreferencesSchema } from '~common/schemas'; +import zodResolver from '~renderer/utils/zodResolver'; +import { api } from '~renderer/utils/api'; +import { useT } from '~renderer/i18n'; + +import TextButton from './styled/TextButton'; +import FilePickerInput from './form/FilePickerInput'; +import CheckboxInput from './form/CheckboxInput'; +import CloseButton from './styled/CloseButton'; + +type Props = { close: () => void }; + +const ClientDirDialog = ({ close }: Props) => { + const t = useT(); + const { data: pref } = api.preferences.get.useQuery(); + const setPref = api.preferences.set.useMutation(); + const isValidClientDir = api.preferences.isValidClientDir.useQuery( + pref?.clientDir, + { enabled: !!pref?.isPortable } + ); + + const verify = api.updater.verify.useMutation(); + + const { + register, + handleSubmit, + watch, + formState, + setValue, + setError, + reset + } = useForm({ + defaultValues: { clientDir: pref?.clientDir ?? '' }, + resolver: zodResolver(PreferencesSchema.pick({ clientDir: true })) + }); + + const chosen = watch('clientDir'); + const [acceptEmpty, setAcceptEmpty] = useState(false); + + const chosenIsClient = api.preferences.isValidClientDir.useQuery(chosen, { + enabled: !!chosen && !pref?.isPortable + }); + const needsEmptyConfirm = + !!chosen && chosenIsClient.isFetched && chosenIsClient.data === false; + + useEffect(() => { + setAcceptEmpty(false); + }, [chosen]); + + useEffect(() => { + pref && reset(pref); + }, [reset, pref]); + + if (pref?.isPortable) { + return ( +
+ +

+ {t('prefs.installLocationTitle')} +

+

{t('prefs.portableInfo')}

+ {!isValidClientDir.isLoading && !isValidClientDir.data && ( +

+ {t('prefs.errorLabel')} + {t('prefs.wowExeNotFound', { exe: 'WoW.exe' })} +

+ )} + + ); + } + + return ( +
{ + if (needsEmptyConfirm && !acceptEmpty) return; + try { + await setPref.mutateAsync({ clientDir }); + verify.mutate(); + close(); + } catch (e) { + setError('clientDir', { + message: e instanceof Error ? e.message : JSON.stringify(e) + }); + } + })} + > + { + reset(); + close(); + }} + /> +

{t('prefs.installLocationTitle')}

+
+ +

{t('prefs.selectDirectory')}

+

{t('prefs.upgradeExisting')}

+
+ + + setValue('clientDir', v, { + shouldTouch: true, + shouldDirty: true, + shouldValidate: true + }) + } + options={{ properties: ['openDirectory', 'createDirectory'] }} + /> +
+ {formState.errors.clientDir && ( +

+ {formState.errors.clientDir.message} +

+ )} + + {needsEmptyConfirm && ( + <> +

+ {t('prefs.noClientHere', { exe: 'WoW.exe' })} +

+ + + )} + + + {t('prefs.confirm')} + + + ); +}; + +export default ClientDirDialog; diff --git a/src/renderer/components/Header.tsx b/src/renderer/components/Header.tsx new file mode 100644 index 0000000..32438a2 --- /dev/null +++ b/src/renderer/components/Header.tsx @@ -0,0 +1,37 @@ +import OctoLogo from '~renderer/assets/logo.png'; + +import { useT } from '~renderer/i18n'; + +import TextButton from './styled/TextButton'; +import { TabNames, type TabType } from './TabsPanel'; + +type Props = { + activeTab?: TabType; + setActiveTab: (tab?: TabType) => void; +}; + +const Header = ({ activeTab, setActiveTab }: Props) => { + const t = useT(); + return ( +
+ + {TabNames.map(tab => ( + setActiveTab(tab)} + active={activeTab === tab} + className="uppercase" + > + {t(`tab.${tab}`)} + + ))} +
+ ); +}; + +export default Header; diff --git a/src/renderer/components/LanguageDropdown.tsx b/src/renderer/components/LanguageDropdown.tsx new file mode 100644 index 0000000..08bb294 --- /dev/null +++ b/src/renderer/components/LanguageDropdown.tsx @@ -0,0 +1,91 @@ +import { useEffect, useRef, useState } from 'react'; +import { Globe } from 'lucide-react'; +import { createPortal } from 'react-dom'; +import cls from 'classnames'; + +import { api } from '~renderer/utils/api'; +import { useLocale } from '~renderer/i18n'; +import { type Lang } from '~renderer/i18n/translations'; + +const LOCALES: { value: Lang; code: string; label: string }[] = [ + { value: 'enUS', code: 'En', label: 'English' }, + { value: 'deDE', code: 'De', label: 'Deutsch' }, + { value: 'zhCN', code: 'Zh', label: '中文' }, + { value: 'esES', code: 'Es', label: 'Español' }, + { value: 'ptBR', code: 'Pt', label: 'Português' }, + { value: 'ruRU', code: 'Ru', label: 'Русский' } +]; + +const LanguageDropdown = () => { + const { lang, setLang, t } = useLocale(); + const setPref = api.preferences.set.useMutation(); + const code = LOCALES.find(l => l.value === lang)?.code ?? 'En'; + + const [open, setOpen] = useState(false); + const [pos, setPos] = useState<{ top: number; right: number }>(); + const btnRef = useRef(null); + + const toggle = () => { + const r = btnRef.current?.getBoundingClientRect(); + if (r) setPos({ top: r.bottom + 4, right: window.innerWidth - r.right }); + setOpen(o => !o); + }; + + useEffect(() => { + if (!open) return; + const close = () => setOpen(false); + window.addEventListener('click', close); + return () => window.removeEventListener('click', close); + }, [open]); + + const pick = (v: Lang) => { + setLang(v); + setPref.mutate({ locale: v }); + setOpen(false); + }; + + return ( + <> + + {open && + pos && + createPortal( +
e.stopPropagation()} + style={{ top: pos.top, right: pos.right }} + className="fixed z-50 flex flex-col border border-blueGray/30 bg-darkGray py-1 shadow-[0_8px_20px_rgba(0,0,0,0.5)]" + > + {LOCALES.map(l => ( + + ))} +
, + document.body + )} + + ); +}; + +export default LanguageDropdown; diff --git a/src/renderer/components/LaunchPanel.tsx b/src/renderer/components/LaunchPanel.tsx new file mode 100644 index 0000000..4f5cadd --- /dev/null +++ b/src/renderer/components/LaunchPanel.tsx @@ -0,0 +1,268 @@ +import { useState, type ReactElement } from 'react'; +import cls from 'classnames'; +import log from 'electron-log/renderer'; + +import { type UpdaterStatus, type ModsStatus } from '~main/types'; +import { formatFileSize } from '~common/utils'; +import { api } from '~renderer/utils/api'; +import { useT } from '~renderer/i18n'; + +import Button from './styled/Button'; +import DialogButton from './styled/DialogButton'; +import ClientDirDialog from './ClientDirDialog'; + +const formatDuration = (seconds: number) => { + const s = Math.max(0, Math.round(seconds)); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const rem = s % 60; + if (m < 60) return rem ? `${m}m ${rem}s` : `${m}m`; + const h = Math.floor(m / 60); + const minRem = m % 60; + return minRem ? `${h}h ${minRem}m` : `${h}h`; +}; + +const formatPercent = (progress: number) => + `${parseFloat((progress * 100).toFixed(1))}%`; + +const ProgressDetails = ({ status }: { status: UpdaterStatus }) => { + const t = useT(); + const { bytesDone, bytesTotal, bytesPerSecond, etaSeconds, progress } = + status; + if (bytesTotal === undefined || bytesDone === undefined) return null; + + const pct = + progress !== undefined && progress >= 0 ? formatPercent(progress) : '—'; + + return ( +

+ {pct} + + {' '} + · {formatFileSize(bytesDone)} / {formatFileSize(bytesTotal)} + + {bytesPerSecond !== undefined && bytesPerSecond > 0 && ( + · {formatFileSize(bytesPerSecond, 1)}/s + )} + + {' · '} + {etaSeconds !== undefined + ? `~${formatDuration(etaSeconds)} ${t('launch.remaining')}` + : t('launch.calculating')} + +

+ ); +}; + +const LaunchPanel = () => { + const t = useT(); + const [status, setStatus] = useState({ state: 'verifying' }); + api.updater.observe.useSubscription(undefined, { + onData: setStatus, + onError: err => log.error('Updater subscription error:', err) + }); + + const { data: pref } = api.preferences.get.useQuery(); + + const [modsStatus, setModsStatus] = useState(); + api.mods.observe.useSubscription(undefined, { + onData: setModsStatus + }); + + const verify = api.updater.verify.useMutation(); + const update = api.updater.update.useMutation(); + const start = api.launcher.start.useMutation(); + const applyMods = api.mods.applyAll.useMutation(); + + const modRows = modsStatus?.mods ?? []; + const enabledIds = new Set(modRows.filter(m => m.enabled).map(m => m.id)); + const missingDeps = [ + ...new Set( + modRows + .filter(m => m.enabled) + .flatMap(m => m.requires.filter(d => !enabledIds.has(d))) + ) + ]; + const modName = (id: string) => modRows.find(m => m.id === id)?.name ?? id; + + const props: Record< + UpdaterStatus['state'], + { button: ReactElement; helperText?: ReactElement } + > = { + verifying: { button: }, + serverUnreachable: { + button: pref?.version ? ( + + ) : ( + + ), + helperText: ( +
+

+ {t('launch.errorLabel')}{' '} + {t('launch.serverFail')} +

+

+ {pref?.version + ? t('launch.localVersion', { version: pref.version }) + : t('launch.tryLater')} +

+
+ ) + }, + noClient: { + button: ( + } + > + {open => ( + + )} + + ) + }, + updateAvailable: { + button: ( + + ), + helperText: ( +
+

{t('launch.updateAvailable')}

+

+ {status.progress !== undefined && + status.bytesDone !== undefined && + status.bytesTotal !== undefined && ( + <> + + {formatPercent(status.progress)} + + + {' '} + · {formatFileSize(status.bytesDone)} /{' '} + {formatFileSize(status.bytesTotal)} {t('launch.onDisk')} ·{' '} + + + )} + {status.message} +

+
+ ) + }, + updating: { + button: , + helperText: ( +
+ {status.message && ( +

{status.message}

+ )} + +
+ ) + }, + upToDate: { + button: modsStatus?.dirty ? ( + + ) : ( + + ), + helperText: ( +
+ {modsStatus?.dirty ? ( + missingDeps.length ? ( +

+ {t('mods.enableRequired', { + mods: missingDeps.map(modName).join(', ') + })} +

+ ) : ( +

{t('launch.modsChanged')}

+ ) + ) : ( +

{t('launch.upToDate')}

+ )} +

+ {t('launch.version', { version: pref?.version ?? '' })} +

+
+ ) + }, + failed: { + button: ( + + ), + helperText: ( +
+

+ {t('launch.errorLabel')}{' '} + {status.message} +

+

{t('launch.verifyHint')}

+
+ ) + } + }; + + return ( +
+
+ {props[status.state].helperText ?? + (status.message && ( +

{status.message}

+ ))} + {start.data && !start.data.ok && start.data.error && ( +

{start.data.error}

+ )} +
+ {status.progress !== undefined && ( +
+ )} +
+
+ {props[status.state].button} +
+ ); +}; + +export default LaunchPanel; diff --git a/src/renderer/components/PreferencesDialog.tsx b/src/renderer/components/PreferencesDialog.tsx new file mode 100644 index 0000000..cbeb6bd --- /dev/null +++ b/src/renderer/components/PreferencesDialog.tsx @@ -0,0 +1,229 @@ +import { useForm } from 'react-hook-form'; +import { useEffect, useState } from 'react'; +import { + FilePen, + FolderOpen, + HelpCircle, + RefreshCw, + ScrollText, + ShieldAlert, + ShieldCheck +} from 'lucide-react'; + +import { PreferencesSchema } from '~common/schemas'; +import { api } from '~renderer/utils/api'; +import zodResolver from '~renderer/utils/zodResolver'; +import { useT } from '~renderer/i18n'; + +import TextButton from './styled/TextButton'; +import CheckboxInput from './form/CheckboxInput'; +import DialogButton from './styled/DialogButton'; +import ClientDirDialog from './ClientDirDialog'; +import CloseButton from './styled/CloseButton'; + +const MirrorStatus = () => { + const t = useT(); + const [state, setState] = useState('verifying'); + api.updater.observe.useSubscription(undefined, { + onData: ({ state }) => setState(state) + }); + + if (state === 'serverUnreachable') + return {t('prefs.mirrorOffline')}; + if (state === 'verifying' || state === 'updating') + return ( + {t('prefs.mirrorChecking')} + ); + return {t('prefs.mirrorOnline')}; +}; + +type Props = { close: () => void }; + +const PreferencesDialog = ({ close }: Props) => { + const t = useT(); + const { data: pref } = api.preferences.get.useQuery(); + const setPref = api.preferences.set.useMutation(); + + const verify = api.updater.verify.useMutation(); + const repair = api.mods.repair.useMutation(); + const openInstallFolder = api.general.openInstallFolder.useMutation(); + const openLogFile = api.general.openLogFile.useMutation(); + const addExclusion = api.general.addDefenderExclusion.useMutation(); + + const { handleSubmit, watch, setValue, reset } = useForm({ + defaultValues: pref ?? {}, + resolver: zodResolver(PreferencesSchema) + }); + const [saveError, setSaveError] = useState(null); + + useEffect(() => { + pref && reset(pref); + }, [reset, pref]); + + const setBool = (key: keyof PreferencesSchema) => (v: boolean) => + setValue(key, v, { + shouldTouch: true, + shouldDirty: true, + shouldValidate: true + }); + + return ( +
{ + setSaveError(null); + try { + await setPref.mutateAsync({ + cleanWdb: v.cleanWdb, + minimizeToTrayOnPlay: v.minimizeToTrayOnPlay, + shareDownloads: v.shareDownloads + }); + close(); + } catch (e) { + setSaveError(e instanceof Error ? e.message : String(e)); + } + })} + > + { + reset(); + close(); + }} + /> +

{t('prefs.title')}

+
+ +
+

{t('prefs.installLocation')}

+ openInstallFolder.mutateAsync()} + className="!p-1 text-blueGray" + > + {t('prefs.openFolder')} + +
+
+ + {pref?.clientDir ?? t('prefs.notSelected')} + + ( + { + closeInner(); + close(); + }} + /> + )} + clickAway={pref?.isPortable} + > + {open => ( + + {t('prefs.change')} + + )} + +
+ +
+

{t('prefs.downloadMirror')}

+
+
+ + Iceland + + verify.mutateAsync()} + title={t('prefs.recheck')} + className="!p-0 text-blueGray" + /> +
+ +
+
+

{t('prefs.troubleshooting')}

+ repair.mutateAsync().then(close)} + className="!items-start text-left text-warmGreen" + > + {t('prefs.verifyGameFiles')} + + openLogFile.mutateAsync()} + className="!items-start text-left text-pink" + > + {t('prefs.openLogFile')} + +
+ addExclusion.mutateAsync()} + loading={addExclusion.isLoading} + className="!items-start text-left text-orange" + > + {t('prefs.allowThroughAntivirus')} + + {/* sits on the label's first line even when a locale wraps it */} + window.dispatchEvent(new Event('av-help'))} + title={t('av.whatAllowDoesTitle')} + className="mt-[14px] !p-0 text-yellow hocus:!text-yellow" + /> +
+ {addExclusion.data?.ok === true && ( + + {t('prefs.exclusionAdded')} + + )} + {addExclusion.data?.ok === false && ( + {addExclusion.data.error} + )} +
+ +
+

{t('prefs.generalSettings')}

+ + + +
+
+ + {saveError && ( + {saveError} + )} + + {t('prefs.save')} + + + ); +}; + +export default PreferencesDialog; diff --git a/src/renderer/components/SelfUpdateBanner.tsx b/src/renderer/components/SelfUpdateBanner.tsx new file mode 100644 index 0000000..768376f --- /dev/null +++ b/src/renderer/components/SelfUpdateBanner.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; + +import { api } from '~renderer/utils/api'; +import { useT } from '~renderer/i18n'; + +import Button from './styled/Button'; + +type Status = + | { state: 'idle'; currentVersion: string } + | { state: 'checking'; currentVersion: string } + | { state: 'unavailable'; currentVersion: string } + | { state: 'available'; currentVersion: string; nextVersion: string } + | { + state: 'downloading'; + currentVersion: string; + nextVersion: string; + progress: number; + } + | { state: 'ready'; currentVersion: string; nextVersion: string } + | { state: 'error'; currentVersion: string; message: string }; + +const SelfUpdateBanner = () => { + const t = useT(); + const [status, setStatus] = useState({ + state: 'idle', + currentVersion: '' + }); + api.selfUpdater.observe.useSubscription(undefined, { + onData: setStatus + }); + const install = api.selfUpdater.install.useMutation(); + + if ( + status.state === 'idle' || + status.state === 'checking' || + status.state === 'unavailable' + ) { + return null; + } + + const tone = status.state === 'error' ? 'border-red/40' : 'border-tw/40'; + const label = + status.state === 'error' + ? t('misc.selfUpdateCheckFailed', { message: status.message }) + : status.state === 'available' + ? t('misc.selfUpdateAvailable', { + version: status.nextVersion + }) + : status.state === 'downloading' + ? t('misc.selfUpdateDownloading', { + version: status.nextVersion, + percent: Math.round(status.progress * 100) + }) + : status.state === 'ready' + ? t('misc.selfUpdateReady', { version: status.nextVersion }) + : ''; + + return ( +
+ {label} + {status.state === 'ready' && ( + + )} +
+ ); +}; + +export default SelfUpdateBanner; diff --git a/src/renderer/components/TabErrorBoundary.tsx b/src/renderer/components/TabErrorBoundary.tsx new file mode 100644 index 0000000..cc2aebe --- /dev/null +++ b/src/renderer/components/TabErrorBoundary.tsx @@ -0,0 +1,94 @@ +import { AlertTriangle, RefreshCw } from 'lucide-react'; +import { Component, type ErrorInfo, type ReactNode } from 'react'; +import log from 'electron-log/renderer'; + +import { useT } from '~renderer/i18n'; + +import TextButton from './styled/TextButton'; + +type Props = { + tabName: string; + children: ReactNode; +}; + +type State = { + error?: Error; + componentStack?: string; +}; + +type FallbackProps = { + tabName: string; + error: Error; + componentStack?: string; + onReset: () => void; +}; + +const TabErrorFallback = ({ + tabName, + error, + componentStack, + onReset +}: FallbackProps) => { + const t = useT(); + return ( +
+
+ +

{t('misc.tabCrashed', { tab: tabName })}

+
+
+

+ {error.name}: {error.message} +

+ {componentStack && ( +
+					{componentStack.trim()}
+				
+ )} +
+ + {t('misc.tryAgain')} + +
+ ); +}; + +class TabErrorBoundary extends Component { + state: State = {}; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + log.error(`Tab "${this.props.tabName}" crashed:`, error, info); + this.setState({ error, componentStack: info.componentStack ?? undefined }); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.tabName !== this.props.tabName) { + this.setState({ error: undefined, componentStack: undefined }); + } + } + + #reset = () => this.setState({ error: undefined, componentStack: undefined }); + + render() { + if (!this.state.error) return this.props.children; + const { error, componentStack } = this.state; + return ( + + ); + } +} + +export default TabErrorBoundary; diff --git a/src/renderer/components/TabsPanel.tsx b/src/renderer/components/TabsPanel.tsx new file mode 100644 index 0000000..6ffb289 --- /dev/null +++ b/src/renderer/components/TabsPanel.tsx @@ -0,0 +1,30 @@ +import AddonsTab from './tabs/AddonsTab'; +import ModsTab from './tabs/ModsTab'; +import NewsTab from './tabs/NewsTab'; +import TweaksTab from './tabs/TweaksTab'; +import TabErrorBoundary from './TabErrorBoundary'; + +const Tabs = { + news: NewsTab, + tweaks: TweaksTab, + addons: AddonsTab, + mods: ModsTab +} as const; + +export const TabNames = Object.keys(Tabs) as TabType[]; + +export type TabType = keyof typeof Tabs; + +type Props = { activeTab?: TabType }; + +const TabsPanel = ({ activeTab }: Props) => { + const tab: TabType = activeTab ?? 'news'; + const Component = Tabs[tab]; + return ( + + + + ); +}; + +export default TabsPanel; diff --git a/src/renderer/components/TopBar.tsx b/src/renderer/components/TopBar.tsx new file mode 100644 index 0000000..f1c39e5 --- /dev/null +++ b/src/renderer/components/TopBar.tsx @@ -0,0 +1,83 @@ +import { Settings, Minus, X } from 'lucide-react'; +import { useState } from 'react'; + +import { api } from '~renderer/utils/api'; +import { useT } from '~renderer/i18n'; + +import DialogButton from './styled/DialogButton'; +import PreferencesDialog from './PreferencesDialog'; +import TextButton from './styled/TextButton'; +import LanguageDropdown from './LanguageDropdown'; + +const TopBar = () => { + const t = useT(); + const [safeToQuit, setSafeToQuit] = useState(true); + api.updater.observe.useSubscription(undefined, { + onData: ({ state }) => + setSafeToQuit(state !== 'verifying' && state !== 'updating') + }); + + const minimize = api.general.minimize.useMutation(); + const quit = api.general.quit.useMutation(); + return ( +
+
+ + }> + {open => ( + + )} + + minimize.mutateAsync()} + size={16} + className="!p-1" + /> + ( +
+

{t('quit.title')}

+
+

{t('quit.warn')}

+
+ {t('quit.return')} + quit.mutateAsync()} + className="text-red" + > + {t('topbar.quit')} + +
+
+ )} + > + {open => ( + (!safeToQuit ? open() : quit.mutateAsync())} + size={16} + className="!p-1 hocus:text-red" + /> + )} +
+
+
+ ); +}; + +export default TopBar; diff --git a/src/renderer/components/form/CheckboxInput.tsx b/src/renderer/components/form/CheckboxInput.tsx new file mode 100644 index 0000000..89d350f --- /dev/null +++ b/src/renderer/components/form/CheckboxInput.tsx @@ -0,0 +1,57 @@ +import cls from 'classnames'; +import { type ReactNode } from 'react'; + +import TextButton from '../styled/TextButton'; + +// mt centers this 16px box on the 26px label line box +const Checkbox = () => ( + + + + +); + +type Props = { + label?: ReactNode; + value: boolean; + setValue: (v: boolean) => void; + disabled?: boolean; + className?: cls.Value; +}; + +const CheckboxInput = ({ + label, + value, + setValue, + disabled, + className +}: Props) => ( + !disabled && setValue(!value)} + icon={Checkbox} + className={cls( + '!items-start text-left text-blueGray', + { '[&_*]:fill-none': !value, 'pointer-events-none opacity-40': disabled }, + className + )} + > + {label} + +); + +export default CheckboxInput; diff --git a/src/renderer/components/form/FilePickerInput.tsx b/src/renderer/components/form/FilePickerInput.tsx new file mode 100644 index 0000000..63a2f8f --- /dev/null +++ b/src/renderer/components/form/FilePickerInput.tsx @@ -0,0 +1,47 @@ +import cls from 'classnames'; +import { forwardRef, type HTMLProps } from 'react'; +import { AppWindow, FolderOpen } from 'lucide-react'; + +import { api, type RouterInputs } from '~renderer/utils/api'; + +import TextButton from '../styled/TextButton'; + +type Props = HTMLProps & { + setValue: (newVal: string) => void; + options: RouterInputs['general']['filePicker']; +}; + +const FilePickerInput = forwardRef( + ({ setValue, options, className, ...props }, ref) => { + const filePicker = api.general.filePicker.useMutation(); + return ( +
+ + { + const r = await filePicker.mutateAsync(options); + if (r.canceled) return; + setValue(r.path[0]); + }} + /> +
+ ); + } +); + +export default FilePickerInput; diff --git a/src/renderer/components/form/NumberGrabInput.tsx b/src/renderer/components/form/NumberGrabInput.tsx new file mode 100644 index 0000000..fd4b3da --- /dev/null +++ b/src/renderer/components/form/NumberGrabInput.tsx @@ -0,0 +1,59 @@ +import cls from 'classnames'; +import { type ChangeEvent, type FocusEvent, type HTMLProps, forwardRef } from 'react'; + +type Props = Omit< + HTMLProps, + 'value' | 'min' | 'max' | 'step' +> & { + setValue: (v: number) => void; + min?: number; + max?: number; + step?: number; + sensitivity?: number; +}; +const NumberGrabInput = forwardRef( + ( + { + setValue, + className, + max = Infinity, + min = -Infinity, + step: _step, + sensitivity: _sensitivity, + type: _ignored, + onChange, + onBlur, + ...props + }, + ref + ) => ( + ) => { + const n = Number(e.currentTarget.value); + if (Number.isFinite(n) && n > max) { + e.currentTarget.value = String(max); + } + onChange?.(e); + }} + onBlur={(e: FocusEvent) => { + const n = Number(e.currentTarget.value); + const clamped = Math.max( + Math.min(Number.isFinite(n) ? n : min, max), + min + ); + setValue(clamped); + onBlur?.(e); + }} + onWheel={e => !e.shiftKey && e.currentTarget.blur()} + className={cls( + className, + 'w-[70px] cursor-text border-b border-blueGray bg-inherit p-1 text-center hocus:border-orange' + )} + /> + ) +); +export default NumberGrabInput; diff --git a/src/renderer/components/form/RadioInput.tsx b/src/renderer/components/form/RadioInput.tsx new file mode 100644 index 0000000..5a378f5 --- /dev/null +++ b/src/renderer/components/form/RadioInput.tsx @@ -0,0 +1,44 @@ +import cls from 'classnames'; + +import TextButton from '../styled/TextButton'; + +const Radio = () => ( + + + + +); + +type Props = { + value: T; + setValue: (val: T) => void; + options: { label: string; value: T }[]; +}; + +const RadioInput = ({ value, setValue, options }: Props) => ( +
+ {options.map(o => ( + setValue(o.value)} + icon={Radio} + className={cls('text-blueGray', { + '[&_*]:fill-none': value !== o.value + })} + > + {o.label} + + ))} +
+); + +export default RadioInput; diff --git a/src/renderer/components/form/TextInput.tsx b/src/renderer/components/form/TextInput.tsx new file mode 100644 index 0000000..e3e6e00 --- /dev/null +++ b/src/renderer/components/form/TextInput.tsx @@ -0,0 +1,17 @@ +import cls from 'classnames'; +import { forwardRef, type HTMLProps } from 'react'; + +const TextInput = forwardRef>( + (props, ref) => ( + + ) +); + +export default TextInput; diff --git a/src/renderer/components/styled/Button.tsx b/src/renderer/components/styled/Button.tsx new file mode 100644 index 0000000..36ea7c7 --- /dev/null +++ b/src/renderer/components/styled/Button.tsx @@ -0,0 +1,44 @@ +import type { ButtonHTMLAttributes } from 'react'; +import cls from 'classnames'; +import { type LucideIcon } from 'lucide-react'; + +import IconSpinner from './IconSpinner'; + +type Props = ButtonHTMLAttributes & { + primary?: boolean; + loading?: boolean; + disabled?: boolean; + icon?: LucideIcon; +}; + +const Button = ({ + primary, + loading, + disabled, + icon: Icon, + children, + className, + ...props +}: Props) => ( + +); + +export default Button; diff --git a/src/renderer/components/styled/CloseButton.tsx b/src/renderer/components/styled/CloseButton.tsx new file mode 100644 index 0000000..028c040 --- /dev/null +++ b/src/renderer/components/styled/CloseButton.tsx @@ -0,0 +1,14 @@ +import { X } from 'lucide-react'; + +import TextButton from './TextButton'; + +const CloseButton = ({ close }: { close: () => void }) => ( + +); +export default CloseButton; diff --git a/src/renderer/components/styled/ColoredText.tsx b/src/renderer/components/styled/ColoredText.tsx new file mode 100644 index 0000000..5ee7968 --- /dev/null +++ b/src/renderer/components/styled/ColoredText.tsx @@ -0,0 +1,72 @@ +type Run = { text: string; color?: string }; + +// Keep the WoW "|c" color runs, strip every other "|" escape (textures, links, pipes). +const ESCAPE_RE = + /\|\||\|c([0-9a-f]{8})|\|r|\|T[^|]*\|t|\|H[^|]*\|h|\|h|\|./gi; + +const tokenize = (s: string): Run[] => { + const runs: Run[] = []; + let color: string | undefined; + let buf = ''; + let i = 0; + + const flush = () => { + if (buf) runs.push({ text: buf, color }); + buf = ''; + }; + + let m: RegExpExecArray | null; + while ((m = ESCAPE_RE.exec(s)) !== null) { + buf += s.slice(i, m.index); + i = ESCAPE_RE.lastIndex; + + const tok = m[0]; + if (tok === '||') { + buf += '|'; + } else if (m[1]) { + // drop the leading alpha byte, keep RGB + flush(); + color = `#${m[1].slice(2).toLowerCase()}`; + } else if (tok.toLowerCase() === '|r') { + flush(); + color = undefined; + } + } + buf += s.slice(i); + flush(); + return runs.filter(r => r.text.length > 0); +}; + +export const stripColorCodes = (s: string) => + tokenize(s) + .map(r => r.text) + .join(''); + +export const ColoredText = ({ + children, + className, + style +}: { + children: string; + className?: string; + style?: React.CSSProperties; +}) => { + const runs = tokenize(children); + return ( +

+ {runs.map((r, i) => + r.color ? ( + + {r.text} + + ) : ( + {r.text} + ) + )} +

+ ); +}; diff --git a/src/renderer/components/styled/DialogButton.tsx b/src/renderer/components/styled/DialogButton.tsx new file mode 100644 index 0000000..f492ab1 --- /dev/null +++ b/src/renderer/components/styled/DialogButton.tsx @@ -0,0 +1,79 @@ +import cls from 'classnames'; +import { + useRef, + type ReactElement, + useEffect, + useCallback, + type FC, + isValidElement +} from 'react'; +import { createPortal } from 'react-dom'; + +type Props = { + clickAway?: boolean; + noBlur?: boolean; + focusOnOpen?: boolean; + afterClose?: () => void; + dialog: ReactElement | ((close: () => void) => ReactElement); + children: ReactElement | ((open: () => void) => ReactElement); +}; + +const DialogButton = ({ + clickAway, + noBlur, + focusOnOpen, + afterClose, + dialog, + children +}: Props) => { + const ref = useRef(null); + + const open = useCallback(() => { + if (!ref.current) return; + !focusOnOpen && (ref.current.inert = true); + ref.current.showModal(); + !focusOnOpen && (ref.current.inert = false); + }, [focusOnOpen]); + + const close = useCallback(() => { + ref.current?.close(); + }, []); + + useEffect(() => { + if (!clickAway) return; + const callback = (e: MouseEvent) => e.target === ref.current && close(); + window.addEventListener('click', callback); + return () => window.removeEventListener('click', callback); + }, [clickAway, close]); + + useEffect(() => { + const callback = () => { + afterClose?.(); + return (document.activeElement as HTMLElement)?.blur(); + }; + const r = ref.current; + r?.addEventListener('close', callback); + return () => r?.removeEventListener('close', callback); + }, [afterClose]); + + return ( + <> + {createPortal( + e.stopPropagation()} + className={cls( + 'h-full w-full items-center justify-center bg-[transparent] [&[open]]:flex', + { 'backdrop:backdrop-blur-md': !noBlur } + )} + > + {typeof dialog === 'function' ? dialog(close) : dialog} + , + document.body + )} + {typeof children === 'function' ? children(open) : children} + + ); +}; + +export default DialogButton; diff --git a/src/renderer/components/styled/IconSpinner.tsx b/src/renderer/components/styled/IconSpinner.tsx new file mode 100644 index 0000000..317989e --- /dev/null +++ b/src/renderer/components/styled/IconSpinner.tsx @@ -0,0 +1,8 @@ +import cls from 'classnames'; +import { Loader2, type LucideProps } from 'lucide-react'; + +const IconSpinner = ({ className, ...props }: LucideProps) => ( + +); + +export default IconSpinner; diff --git a/src/renderer/components/styled/TextButton.tsx b/src/renderer/components/styled/TextButton.tsx new file mode 100644 index 0000000..890deed --- /dev/null +++ b/src/renderer/components/styled/TextButton.tsx @@ -0,0 +1,66 @@ +import cls from 'classnames'; +import { type LucideIcon } from 'lucide-react'; +import { type ReactNode } from 'react'; + +import IconSpinner from './IconSpinner'; + +type Props = { + active?: boolean; + loading?: boolean; + disabled?: boolean; + size?: number; + className?: cls.Value; + style?: React.CSSProperties; +} & ( + | { type: 'submit'; onClick?: never } + | { type?: never; onClick: () => void } +) & + ( + | { children: ReactNode; icon?: LucideIcon; title?: never } + | { children?: never; icon: LucideIcon; title: string } + ); + +const TextButton = ({ + title, + type, + active, + loading, + disabled, + icon: Icon, + size, + onClick, + className, + children, + ...props +}: Props) => ( + +); + +export default TextButton; diff --git a/src/renderer/components/tabs/AddonsTab.tsx b/src/renderer/components/tabs/AddonsTab.tsx new file mode 100644 index 0000000..7041afe --- /dev/null +++ b/src/renderer/components/tabs/AddonsTab.tsx @@ -0,0 +1,155 @@ +import { useState } from 'react'; +import { Plus, RefreshCw, Search } from 'lucide-react'; + +import { type AddonData, type AddonsStatus } from '~main/types'; +import { api } from '~renderer/utils/api'; +import TextButton from '~renderer/components/styled/TextButton'; +import useScrollHint from '~renderer/utils/useScrollHint'; +import { useT } from '~renderer/i18n'; + +import DialogButton from '../styled/DialogButton'; +import IconSpinner from '../styled/IconSpinner'; + +import AddonList from './addons/AddonList'; +import { type Dependencies } from './addons/AddonListItem'; +import CustomAddonDialog from './addons/CustomAddonDialog'; + +const RECOMMENDED = new Set([ + 'AtlasLoot', + 'pfExtend', + 'pfQuest', + 'pfQuest-turtle', + 'SellValue', + 'ShaguTweaks', + 'ShaguTweaks-extras', + 'TurtleMail' +]); + +const localeFilter = (l: AddonData[], filter: string) => { + const seen = new Set(); + const deduped = l.filter(a => { + if (seen.has(a.folder)) return false; + seen.add(a.folder); + return true; + }); + return deduped + .filter(a => + a.folder.toLocaleLowerCase().includes(filter.toLocaleLowerCase()) + ) + .sort((a, b) => a.folder.localeCompare(b.folder)); +}; + +const AddonsTab = () => { + const t = useT(); + const [data, setData] = useState({ + state: 'verifying', + addons: {}, + available: [] + }); + api.addons.observe.useSubscription(undefined, { onData: setData }); + + const isUpdateAvailable = Object.values(data.addons).some( + a => a.status === 'outOfDate' || a.status === 'downloading' + ); + const dependencies: Dependencies = Object.fromEntries([ + ...data.available.map(a => [a.folder, 'available']), + ...Object.values(data.addons).map(a => [ + a.folder, + a.progress ?? (a.status === 'upToDate' ? 'installed' : 'available') + ]) + ]); + + const [filter, setFilter] = useState(''); + + const verify = api.addons.verify.useMutation(); + const update = api.addons.update.useMutation(); + + const scrollRef = useScrollHint(); + + return ( +
+
+ + !(a.folder in data.addons) && RECOMMENDED.has(a.folder) + ), + filter + )} + dependencies={dependencies} + /> + !(a.folder in data.addons) && !RECOMMENDED.has(a.folder) + ), + filter + )} + dependencies={dependencies} + /> +
+
+
+ verify.mutateAsync()} + className="-ml-2 text-blueGray" + icon={RefreshCw} + size={18} + loading={data.state !== 'done'} + > + {t('addons.checkForUpdates')} + + } + > + {open => ( + + {t('addons.addCustomGitAddon')} + + )} + + {data.state === 'verifying' ? ( + + ) : isUpdateAvailable ? ( + update.mutateAsync({})} + className="justify-self-end text-warmGreen" + > + {t('addons.updateAll')} + + ) : ( +

+ {t('addons.everythingUpToDate')} +

+ )} +
+
+
+ setFilter(e.target.value)} + /> + +
+
+
+ ); +}; +export default AddonsTab; diff --git a/src/renderer/components/tabs/ComingSoonTab.tsx b/src/renderer/components/tabs/ComingSoonTab.tsx new file mode 100644 index 0000000..32c1853 --- /dev/null +++ b/src/renderer/components/tabs/ComingSoonTab.tsx @@ -0,0 +1,12 @@ +import { useT } from '~renderer/i18n'; + +const ComingSoonTab = () => { + const t = useT(); + return ( +
+

{t('misc.comingSoon')}

+
+ ); +}; + +export default ComingSoonTab; diff --git a/src/renderer/components/tabs/ModsTab.tsx b/src/renderer/components/tabs/ModsTab.tsx new file mode 100644 index 0000000..5542258 --- /dev/null +++ b/src/renderer/components/tabs/ModsTab.tsx @@ -0,0 +1,304 @@ +import { useEffect, useRef, useState } from 'react'; +import { ExternalLink, AlertTriangle, Sparkles } from 'lucide-react'; +import { createPortal } from 'react-dom'; +import cls from 'classnames'; + +import { api } from '~renderer/utils/api'; +import useScrollHint from '~renderer/utils/useScrollHint'; +import { useT } from '~renderer/i18n'; +import { + type ModRowStatus, + type ModsStatus, + type CustomMod +} from '~main/types'; + +import TextButton from '../styled/TextButton'; +import CheckboxInput from '../form/CheckboxInput'; +import IconSpinner from '../styled/IconSpinner'; + +const RowState = ({ row }: { row: ModRowStatus }) => { + const t = useT(); + if (['downloading', 'installing', 'uninstalling'].includes(row.state)) + return ; + if (row.state === 'error') + return ( + + + + ); + if ( + row.installedVersion && + row.installedVersion !== row.latestVersion && + !row.ignoreUpdates + ) + return {t('mods.update')}; + return null; +}; + +const ModRow = ({ row }: { row: ModRowStatus }) => { + const t = useT(); + const toggle = api.mods.toggle.useMutation(); + const setIgnore = api.mods.setIgnoreUpdates.useMutation(); + const openLink = api.general.openLink.useMutation(); + + return ( + <> +
+ {row.recommended && ( + + )} + + {row.name} + + {row.latestVersion} +
+ toggle.mutate({ id: row.id, enabled: v })} + className="justify-self-center" + /> +
+

{row.description}

+ openLink.mutateAsync(row.repoUrl)} + className="!p-0 text-blueGray" + /> + +
+ setIgnore.mutate({ id: row.id, ignore: v })} + label={{t('mods.ignoreUpdates')}} + /> + + ); +}; + +const CustomRow = ({ row }: { row: CustomMod }) => { + const toggle = api.mods.toggleCustom.useMutation(); + return ( + <> + {row.name} + toggle.mutate({ name: row.name, enabled: v })} + className="justify-self-center" + /> + + ); +}; + +const AddDllButton = () => { + const t = useT(); + const pick = api.general.filePicker.useMutation(); + const add = api.mods.addCustomDll.useMutation(); + const [error, setError] = useState(null); + + const onClick = async () => { + setError(null); + const res = await pick.mutateAsync({ + title: t('mods.addDllTitle'), + filters: [{ name: 'DLL', extensions: ['dll'] }], + properties: ['openFile'] + }); + if (res.canceled) return; + const result = await add.mutateAsync({ path: res.path[0] }); + if (!result.ok) setError(result.error ?? t('mods.addDllFailed')); + }; + + return ( +
+ {error && {error}} + + {t('mods.addDll')} + +
+ ); +}; + +const ModsTab = () => { + const t = useT(); + const [status, setStatus] = useState(); + api.mods.observe.useSubscription(undefined, { + onData: setStatus + }); + + const list = api.mods.list.useQuery(undefined, { + refetchOnMount: true + }); + useEffect(() => { + if (!status && list.data) setStatus(list.data); + }, [list.data, status]); + + const apply = api.mods.applyAll.useMutation(); + const resync = api.updater.update.useMutation(); + const revalidate = api.mods.verify.useMutation(); + + const scrollRef = useScrollHint(); + + const mods = status?.mods ?? []; + const enabledIds = new Set(mods.filter(m => m.enabled).map(m => m.id)); + const modName = (id: string) => mods.find(m => m.id === id)?.name ?? id; + const missingDeps = [ + ...new Set( + mods + .filter(m => m.enabled) + .flatMap(m => m.requires.filter(d => !enabledIds.has(d))) + ) + ]; + const pendingDepMessage = missingDeps + .map(dep => { + const requiredBy = mods + .filter(m => m.enabled && m.requires.includes(dep)) + .map(m => m.name) + .join(', '); + return t('mods.depRequired', { mod: modName(dep), requiredBy }); + }) + .join('\n'); + + const dialogRef = useRef(null); + const [shownDepMessage, setShownDepMessage] = useState(null); + + useEffect(() => { + if (shownDepMessage) { + if (!dialogRef.current?.open) dialogRef.current?.showModal(); + (document.activeElement as HTMLElement | null)?.blur(); + } else dialogRef.current?.close(); + }, [shownDepMessage]); + + const [applied, setApplied] = useState(false); + const appliedTimer = useRef(); + useEffect(() => () => window.clearTimeout(appliedTimer.current), []); + const onApply = async () => { + if (missingDeps.length) { + setShownDepMessage(pendingDepMessage); + return; + } + setApplied(false); + await apply.mutateAsync(); + setApplied(true); + window.clearTimeout(appliedTimer.current); + appliedTimer.current = window.setTimeout(() => setApplied(false), 2500); + }; + + const showApply = + !!status?.dirty || apply.isLoading || status?.state === 'busy'; + + return ( +
+
+

{t('mods.title')}

+ {status?.dirty ? ( + {t('mods.unsavedChanges')} + ) : applied ? ( + {t('mods.applied')} + ) : null} +
+

+ {t('mods.warning')} +

+ {missingDeps.length > 0 && ( +

+ ⚠{' '} + {t('mods.enableRequired', { + mods: missingDeps.map(modName).join(', ') + })} +

+ )} + {!!status?.missingFiles?.length && ( +
+ + ⚠ {t('mods.missingFiles', { mods: status.missingFiles.join(', ') })} + + { + await resync.mutateAsync(); + await revalidate.mutateAsync(); + }} + loading={resync.isLoading || revalidate.isLoading} + className="text-warmGreen" + > + {t('mods.reverify')} + +
+ )} +
+
+
+ {status?.mods.map(row => ( + + ))} +
+
+
+

{t('mods.yourDlls')}

+ +
+ {status?.custom?.length ? ( +
+ {status.custom.map(c => ( + + ))} +
+ ) : ( +

{t('mods.yourDllsEmpty')}

+ )} +
+
+
+

+ {status?.dirty ? ( + {t('mods.unsavedChanges')} + ) : applied ? ( + {t('mods.applied')} + ) : ( + <> + {t('mods.highlighted')}{' '} + {t('mods.highlightedRecommended')} + + )} +

+ + {t('mods.apply')} + +
+ {createPortal( + setShownDepMessage(null)} + className="h-full w-full items-center justify-center bg-[transparent] backdrop:backdrop-blur-sm [&[open]]:flex" + > + {shownDepMessage && ( +
+

{t('mods.cantApplyYet')}

+

{shownDepMessage}

+ setShownDepMessage(null)} + className="self-end text-green" + > + {t('mods.close')} + +
+ )} +
, + document.body + )} +
+ ); +}; + +export default ModsTab; diff --git a/src/renderer/components/tabs/NewsTab.tsx b/src/renderer/components/tabs/NewsTab.tsx new file mode 100644 index 0000000..649b6f9 --- /dev/null +++ b/src/renderer/components/tabs/NewsTab.tsx @@ -0,0 +1,122 @@ +import { AlertTriangle, ExternalLink, RefreshCw } from 'lucide-react'; + +import { type NewsItem } from '~main/types'; +import { api } from '~renderer/utils/api'; +import { useT } from '~renderer/i18n'; +import useScrollHint from '~renderer/utils/useScrollHint'; + +import IconSpinner from '../styled/IconSpinner'; +import TextButton from '../styled/TextButton'; + +const formatDate = (raw: string) => { + const d = new Date(raw); + if (Number.isNaN(d.getTime())) return raw; + return d.toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric' + }); +}; + +const NewsEntry = ({ item }: { item: NewsItem }) => { + const t = useT(); + const openLink = api.general.openLink.useMutation(); + return ( +
+
+
{item.title}
+ + {formatDate(item.date)} + +
+ {item.author && ( + + {t('misc.newsByAuthor', { author: item.author })} + + )} +

{item.body}

+ {item.url && ( + openLink.mutateAsync(item.url!)} + > + {t('misc.newsReadMore')} + + )} +
+ ); +}; + +const NewsColumn = ({ forum, title }: { forum: number; title: string }) => { + const t = useT(); + const query = api.news.list.useQuery( + { forum }, + { + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + retry: 1 + } + ); + const scrollRef = useScrollHint(); + + return ( +
+
+

{title}

+ query.refetch()} + title={t('misc.refresh')} + /> +
+
+
+ {query.isLoading ? ( +
+ +

{t('misc.newsLoading')}

+
+ ) : query.isError ? ( +
+ +

{t('misc.newsError')}

+ query.refetch()} + > + {t('misc.tryAgain')} + +
+ ) : !query.data?.length ? ( +
+

{t('misc.newsEmpty')}

+
+ ) : ( + query.data.map(item => ) + )} +
+
+ ); +}; + +const NewsTab = () => { + const t = useT(); + return ( +
+ + +
+ ); +}; + +export default NewsTab; diff --git a/src/renderer/components/tabs/TweaksTab.tsx b/src/renderer/components/tabs/TweaksTab.tsx new file mode 100644 index 0000000..9bec365 --- /dev/null +++ b/src/renderer/components/tabs/TweaksTab.tsx @@ -0,0 +1,248 @@ +import { useForm, type UseFormReturn } from 'react-hook-form'; +import { useEffect } from 'react'; +import cls from 'classnames'; + +import { api } from '~renderer/utils/api'; +import { ConfigWtfSchema } from '~common/schemas'; +import zodResolver from '~renderer/utils/zodResolver'; +import useScrollHint from '~renderer/utils/useScrollHint'; +import { useT } from '~renderer/i18n'; + +import TextButton from '../styled/TextButton'; +import CheckboxInput from '../form/CheckboxInput'; +import NumberGrabInput from '../form/NumberGrabInput'; + +type ItemProps = { + type?: 'checkbox' | 'number'; + id: keyof ConfigWtfSchema; + label: string; + recommended?: boolean; + text: string; + min?: number; + max?: number; + step?: number; + sensitivity?: number; + form: UseFormReturn; +}; + +const Item = ({ + type = 'checkbox', + id, + label, + recommended, + text, + form, + ...props +}: ItemProps) => { + const { watch, setValue, register } = form; + const setOpts = { + shouldTouch: true, + shouldDirty: true, + shouldValidate: true + } as const; + const watched = type === 'checkbox' ? watch(id) : undefined; + const registered = type === 'number' ? register(id) : undefined; + return ( + <> +

{label}

+ {type === 'checkbox' && ( + setValue(id, v, setOpts)} + className="justify-self-center" + /> + )} + {type === 'number' && registered && ( + setValue(id, v, setOpts)} + /> + )} +

{text}

+ + ); +}; + +const TweaksTab = () => { + const t = useT(); + const { data: pref } = api.preferences.get.useQuery(); + const setPref = api.preferences.set.useMutation(); + + const applyPatch = api.patcher.apply.useMutation(); + const syncRaidVisuals = api.updater.syncRaidVisuals.useMutation(); + + const form = useForm({ + defaultValues: pref?.config ?? {}, + resolver: zodResolver(ConfigWtfSchema) + }); + const { handleSubmit, reset, formState } = form; + + const { data: hw } = api.general.hardware.useQuery(); + const recommendedFarClip = hw?.recommendedFarClip; + const farClipValue = form.watch('farClip'); + const farClipText = + t('tweaks.farClip.text') + + (recommendedFarClip != null + ? ' ' + t('tweaks.farClip.recommendedHint', { value: recommendedFarClip }) + : ''); + + const isApplying = + setPref.isLoading || applyPatch.isLoading || syncRaidVisuals.isLoading; + + useEffect(() => { + pref && reset(pref.config); + }, [reset, pref]); + + const scrollRef = useScrollHint(); + + return ( +
{ + await setPref.mutateAsync({ config, farClipUserSet: true }); + await applyPatch.mutateAsync(); + await syncRaidVisuals.mutateAsync(); + + reset(config); + })} + className="tw-surface flex min-h-0 flex-grow flex-col gap-3" + > +
+ + + + + +

+ {t('tweaks.cameraHeading')} +

+ + + + + +

+ {t('tweaks.soundsHeading')} +

+ +
+
+
+

+ {applyPatch.isError ? ( + + {t('tweaks.applyFailed', { + message: applyPatch.error?.message ?? '' + })} + + ) : ( + <> + + {t('tweaks.highlighted')} + {' '} + {t('tweaks.recommendedNote')} + + )} +

+ { + const config = + recommendedFarClip != null + ? { + ...ConfigWtfSchema.parse({}), + farClip: recommendedFarClip, + raidVisuals: form.getValues('raidVisuals') + } + : { + ...ConfigWtfSchema.parse({}), + raidVisuals: form.getValues('raidVisuals') + }; + await setPref.mutateAsync({ config, farClipUserSet: false }); + reset(config); + }} + > + {t('tweaks.reset')} + + {(formState.isDirty || isApplying) && ( + + {t('tweaks.apply')} + + )} +
+
+ ); +}; + +export default TweaksTab; diff --git a/src/renderer/components/tabs/addons/AddonDetail.tsx b/src/renderer/components/tabs/addons/AddonDetail.tsx new file mode 100644 index 0000000..f964a1a --- /dev/null +++ b/src/renderer/components/tabs/addons/AddonDetail.tsx @@ -0,0 +1,142 @@ +import { type ReactNode, type PropsWithChildren } from 'react'; +import { + AlertOctagon, + ExternalLink, + X, + AlertTriangle, + Check, + Dot, + DownloadCloud +} from 'lucide-react'; + +import { type AddonData } from '~main/types'; +import { api } from '~renderer/utils/api'; +import TextButton from '~renderer/components/styled/TextButton'; +import { ColoredText } from '~renderer/components/styled/ColoredText'; +import useScrollHint from '~renderer/utils/useScrollHint'; +import IconSpinner from '~renderer/components/styled/IconSpinner'; +import CloseButton from '~renderer/components/styled/CloseButton'; +import { useT } from '~renderer/i18n'; + +import { type LocalDependencies } from './AddonListItem'; + +const AddonDetailItem = ({ + name, + children +}: PropsWithChildren<{ name: string }>) => + children ? ( +
+ {name}:{' '} + {typeof children === 'string' ? ( + {children} + ) : ( + children + )} +
+ ) : null; + +type Props = AddonData & { + close: () => void; + warnings: { full: ReactNode; short: ReactNode }[]; + dependencies: LocalDependencies; +}; + +const AddonDetail = ({ close, warnings, dependencies, ...addon }: Props) => { + const t = useT(); + const openLink = api.general.openLink.useMutation(); + const update = api.addons.update.useMutation(); + + const scrollRef = useScrollHint(); + + return ( +
+ + + {addon.toc?.Title ?? addon.folder} + +
+ {addon.error && ( +

+ {' '} + {addon.error} +

+ )} + {warnings.map((w, i) => ( +

+ {w.full} +

+ ))} + {(addon.toc?.Notes || addon.description) && ( + {addon.toc?.Notes ?? addon.description ?? ''} + )} +
+ + {addon.git && ( + openLink.mutateAsync(addon.git)} + className="s1 -m-2 !inline" + > + {t('addons.openOnGithubShort')} + + + )} + + {addon.toc && ( + <> + + {addon.toc.Author} + + + {addon.toc.Version} + + + {!!dependencies.length && ( +
    + {dependencies.map(({ name, optional, status }) => ( +
  • + {status === 'installed' ? ( + + ) : status === 'available' ? ( + + update.mutateAsync({ toUpdate: [name] }) + } + className="-m-2 !inline text-warmGreen" + /> + ) : status === 'missing' ? ( + optional ? ( + + ) : ( + + ) + ) : ( + + )} +

    {name}

    + {!['installed', 'available', 'missing'].includes( + status + ) ? ( +

    {status}

    + ) : optional ? ( +

    + {t('addons.optional')} +

    + ) : null} +
  • + ))} +
+ )} +
+ + )} +
+
+ ); +}; +export default AddonDetail; diff --git a/src/renderer/components/tabs/addons/AddonItemErrorBoundary.tsx b/src/renderer/components/tabs/addons/AddonItemErrorBoundary.tsx new file mode 100644 index 0000000..da5aaf4 --- /dev/null +++ b/src/renderer/components/tabs/addons/AddonItemErrorBoundary.tsx @@ -0,0 +1,37 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; +import log from 'electron-log/renderer'; + +type Props = { children: ReactNode; folder: string; row: number }; +type State = { hasError: boolean; message?: string }; + +class AddonItemErrorBoundary extends Component { + state: State = { hasError: false }; + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, message: error.message }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + log.error( + `[AddonListItem] crash row=${this.props.row} folder=${this.props.folder}:`, + error, + info + ); + } + + render() { + if (!this.state.hasError) return this.props.children; + return ( +
+
+ Failed to render "{this.props.folder}": {this.state.message ?? 'unknown error'} +
+
+ ); + } +} + +export default AddonItemErrorBoundary; diff --git a/src/renderer/components/tabs/addons/AddonList.tsx b/src/renderer/components/tabs/addons/AddonList.tsx new file mode 100644 index 0000000..228d8e2 --- /dev/null +++ b/src/renderer/components/tabs/addons/AddonList.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { ChevronDown, ChevronRight } from 'lucide-react'; +import cls from 'classnames'; + +import { type AddonData } from '~main/types'; + +import AddonListItem, { type Dependencies } from './AddonListItem'; +import AddonItemErrorBoundary from './AddonItemErrorBoundary'; + +type Props = { + title: string; + addons: AddonData[]; + dependencies: Dependencies; +}; + +const AddonList = ({ title, addons, dependencies }: Props) => { + const [open, setOpen] = useState(true); + if (!addons.length) return null; + return ( +
+ +
+ {addons.map((addon, i) => { + const { ref: gitRef, ...rest } = addon; + return ( + + + + ); + })} +
+
+ ); +}; +export default AddonList; diff --git a/src/renderer/components/tabs/addons/AddonListItem.tsx b/src/renderer/components/tabs/addons/AddonListItem.tsx new file mode 100644 index 0000000..0602761 --- /dev/null +++ b/src/renderer/components/tabs/addons/AddonListItem.tsx @@ -0,0 +1,237 @@ +import { + AlertOctagon, + AlertTriangle, + DownloadCloud, + Github, + HelpCircle, + Trash2 +} from 'lucide-react'; +import cls from 'classnames'; + +import { type AddonData } from '~main/types'; +import { api } from '~renderer/utils/api'; +import TextButton from '~renderer/components/styled/TextButton'; +import { ColoredText } from '~renderer/components/styled/ColoredText'; +import IconSpinner from '~renderer/components/styled/IconSpinner'; +import DialogButton from '~renderer/components/styled/DialogButton'; +import { isNotUndef } from '~common/utils'; +import CloseButton from '~renderer/components/styled/CloseButton'; +import { useT } from '~renderer/i18n'; + +import AddonDetail from './AddonDetail'; + +export type Dependencies = { + [folder: string]: 'installed' | 'available' | string; +}; + +export type LocalDependencies = { + name: string; + optional: boolean; + status: 'installed' | 'available' | 'missing' | string; +}[]; + +type Props = Omit & { + row: number; + dependencies: Dependencies; + gitRef?: string; +}; + +const toRepoUrl = (git?: string) => + git ? git.replace(/\.git$/, '') : undefined; + +const AddonListItem = ({ row, dependencies, ...addon }: Props) => { + const t = useT(); + const update = api.addons.update.useMutation(); + const remove = api.addons.remove.useMutation(); + const openLink = api.general.openLink.useMutation(); + const repoUrl = toRepoUrl(addon.git); + + const localDependencies: LocalDependencies = [ + ...(addon.toc?.Dependencies?.split(', ')?.map(d => [d, false] as const) ?? + []), + ...(addon.toc?.OptionalDeps?.split(', ')?.map(d => [d, true] as const) ?? + []) + ].map(([d, optional]) => ({ + name: d, + optional, + status: dependencies[d] ?? 'missing' + })); + + const warnings = [ + addon.toc && addon.toc?.Interface !== '11200' + ? { + full: t('addons.warnIncorrectVersionFull', { + version: addon.toc?.Interface ?? '' + }), + short: t('addons.warnIncorrectVersionShort') + } + : undefined, + localDependencies.some(d => d.status !== 'installed' && !d.optional) + ? { + full: t('addons.warnMissingDependenciesFull', { + deps: localDependencies + .filter(d => d.status !== 'installed' && !d.optional) + .map(d => d.name) + .join(', ') + }), + short: t('addons.warnMissingDependenciesShort') + } + : undefined + ].filter(isNotUndef); + + return ( +
+
+ {addon.status === 'fetching' ? ( + + ) : ( + ( + + )} + > + {open => ( + + )} + + )} + +
+ {addon.toc?.Title ?? addon.folder} + {repoUrl && ( + openLink.mutateAsync(repoUrl)} + className="!p-1 text-blueGray/60 hocus:text-pink" + /> + )} +
+ + + {addon.toc?.Notes ?? addon.description ?? ''} + + +
+ {addon.status === 'downloading' ? ( + <> +

{addon.progress}

+ + + ) : addon.status === 'invalid' ? ( +

{addon.error}

+ ) : warnings.length ? ( +

{warnings[0].short}

+ ) : ( +

+ {addon.status === 'upToDate' + ? t('addons.upToDate') + : !addon.git + ? t('addons.notVersioned') + : ''} +

+ )} + {addon.status === 'outOfDate' && ( + update.mutateAsync({ toUpdate: [addon.folder] })} + className="s1 -mx-2 justify-self-end" + > + {t('addons.update')} + + )} + {addon.status === 'available' ? ( + update.mutateAsync({ toUpdate: [addon.folder] })} + className="text-warmGreen" + icon={DownloadCloud} + size={18} + title={t('addons.download')} + /> + ) : ( + ( +
+ +

{t('addons.deleteConfirmTitle')}

+
+

+ {t('addons.deleteConfirmBody', { folder: addon.folder })} +

+

+ {t('addons.deleteConfirmFiles')} +

+ { + await remove.mutateAsync({ toDelete: [addon.folder] }); + close(); + }} + disabled={remove.isLoading} + className="self-end text-red" + > + {t('addons.delete')} + +
+ )} + > + {open => ( + + )} +
+ )} +
+
+ ); +}; + +export default AddonListItem; diff --git a/src/renderer/components/tabs/addons/CustomAddonDialog.tsx b/src/renderer/components/tabs/addons/CustomAddonDialog.tsx new file mode 100644 index 0000000..2c60d1e --- /dev/null +++ b/src/renderer/components/tabs/addons/CustomAddonDialog.tsx @@ -0,0 +1,82 @@ +import { Check, X } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +import CloseButton from '~renderer/components/styled/CloseButton'; +import IconSpinner from '~renderer/components/styled/IconSpinner'; +import TextButton from '~renderer/components/styled/TextButton'; +import { api } from '~renderer/utils/api'; +import { useT } from '~renderer/i18n'; + +const useDebounced = (value: string, delay: number) => { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const timeout = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(timeout); + }, [value, delay]); + + return debouncedValue; +}; + +const CustomAddonDialog = ({ close }: { close: () => void }) => { + const t = useT(); + const [url, setUrl] = useState(''); + const debouncedUrl = useDebounced(url, 500); + const response = api.addons.checkGitUrl.useQuery(debouncedUrl, { + enabled: !!debouncedUrl + }); + const update = api.addons.install.useMutation(); + + return ( +
+ +

{t('addons.installAddon')}

+
+ {response.data ? ( + {t('addons.previewAlt')} + ) : ( +
+ {response.isFetching && } +
+ )} +
+ setUrl(e.target.value)} + /> + {response.isFetching ? ( + + ) : response.data ? ( + + ) : ( + + )} +
+
+

+ {response.data + ? t('addons.readyToInstall') + : t('addons.invalidGitUrl')} +

+ { + if (!response.data) return; + update.mutateAsync(response.data); + close(); + setUrl(''); + }} + className={response.data ? 'text-warmGreen' : 'text-blueGray'} + disabled={!response.data || response.isLoading} + > + {t('addons.install')} + +
+
+ ); +}; + +export default CustomAddonDialog; diff --git a/src/renderer/env.d.ts b/src/renderer/env.d.ts new file mode 100644 index 0000000..2f38bb3 --- /dev/null +++ b/src/renderer/env.d.ts @@ -0,0 +1,16 @@ +/// + +interface ImportMetaEnv { + readonly MAIN_VITE_SERVER_URL: string; + // forum base for the news feed; defaults to the live forum so PTR shows real posts + readonly MAIN_VITE_FORUM_URL: string; + readonly MAIN_VITE_CLIENT_VERSION: string; + // PTR realm/patch host; only set for PTR builds, live falls back to octowow.st. + readonly MAIN_VITE_PTR_REALMLIST: string; + // When set, sync the client from this web-seeded .torrent instead of the manifest. + readonly MAIN_VITE_CLIENT_TORRENT_URL: string; + // optional raid-visuals patch (patch-O.mpq); a ".sha256" sidecar drives change detection + readonly MAIN_VITE_RAID_VISUALS_URL: string; + // the content patch (patch-5.mpq), served outside the torrent; kept current by a ".sha256" sidecar + readonly MAIN_VITE_CLIENT_PATCH_URL: string; +} diff --git a/src/renderer/i18n/index.tsx b/src/renderer/i18n/index.tsx new file mode 100644 index 0000000..3ee26f3 --- /dev/null +++ b/src/renderer/i18n/index.tsx @@ -0,0 +1,52 @@ +import { + createContext, + useContext, + useEffect, + useState, + type ReactNode +} from 'react'; + +import { api } from '~renderer/utils/api'; + +import { translations, type Lang } from './translations'; + +type Params = Record; +type Translate = (key: string, params?: Params) => string; + +type LocaleCtx = { + lang: Lang; + setLang: (lang: Lang) => void; + t: Translate; +}; + +const LocaleContext = createContext({ + lang: 'enUS', + setLang: () => {}, + t: key => key +}); + +export const LocaleProvider = ({ children }: { children: ReactNode }) => { + const { data: pref } = api.preferences.get.useQuery(); + const [lang, setLang] = useState('enUS'); + + useEffect(() => { + if (pref?.locale) setLang(pref.locale); + }, [pref?.locale]); + + const t: Translate = (key, params) => { + let s = translations[lang]?.[key] ?? translations.enUS[key] ?? key; + if (params) + for (const [k, v] of Object.entries(params)) + s = s.replace(`{${k}}`, String(v)); + return s; + }; + + return ( + + {children} + + ); +}; + +export const useLocale = () => useContext(LocaleContext); +export const useT = () => useContext(LocaleContext).t; diff --git a/src/renderer/i18n/translations.ts b/src/renderer/i18n/translations.ts new file mode 100644 index 0000000..bb96763 --- /dev/null +++ b/src/renderer/i18n/translations.ts @@ -0,0 +1,1390 @@ +export type Lang = 'enUS' | 'deDE' | 'zhCN' | 'esES' | 'ptBR' | 'ruRU'; + +type Dict = Record; + +const enUS: Dict = { + 'tab.news': 'News', + 'tab.tweaks': 'Tweaks', + 'tab.addons': 'Addons', + 'tab.mods': 'Mods', + 'topbar.settings': 'Settings', + 'topbar.minimize': 'Minimize', + 'topbar.quit': 'Quit', + 'topbar.language': 'Language', + 'quit.title': 'Quit?', + 'quit.warn': + 'Your game is currently being updated. Quitting now may cause problems.', + 'quit.return': 'Return', + 'launch.verifying': 'Verifying', + 'launch.play': 'Play', + 'launch.retry': 'Retry', + 'launch.install': 'Install', + 'launch.update': 'Update', + 'launch.updating': 'Updating', + 'launch.applying': 'Applying', + 'launch.updateAvailable': 'Update available!', + 'launch.modsChanged': 'Mods changed, apply before playing', + 'launch.upToDate': 'Everything up to date!', + 'launch.version': 'Version: {version}', + 'launch.errorLabel': 'Error:', + 'launch.serverFail': 'Failed to reach update server', + 'launch.localVersion': 'You can launch local version {version}', + 'launch.tryLater': 'Please try again later', + 'launch.verifyHint': 'Verify your game data by clicking Retry.', + 'launch.remaining': 'remaining', + 'launch.calculating': 'calculating…', + 'launch.onDisk': 'on disk', + 'tweaks.raidVisuals.label': 'Updated Raid Visuals', + 'tweaks.raidVisuals.text': + 'Optional ~9 MB download. Adds clearer ground markers and sounds for raid boss abilities, kept in sync with the server automatically. (AKA Patch-O)', + 'tweaks.alwaysAutoLoot.label': 'Always auto-loot', + 'tweaks.alwaysAutoLoot.text': + 'Reverses auto-loot behavior to always auto-loot and disable auto-with bound key.', + 'tweaks.largeAddress.label': 'Large Address Aware', + 'tweaks.largeAddress.text': 'Allows the game to use more than 2GB of memory.', + 'tweaks.nameplateRange.label': 'Nameplate range', + 'tweaks.nameplateRange.text': + 'Increases distance at which nameplates are visible. [Vanilla: 20] [Classic: 41]', + 'tweaks.cameraHeading': 'Camera', + 'tweaks.fieldOfView.label': 'Field of View', + 'tweaks.fieldOfView.text': + 'Recommended for widescreen window resolutions. [Vanilla: 90] [Tweaks: 110]', + 'tweaks.farClip.label': 'Render distance', + 'tweaks.farClip.text': + 'Increases maximum render distance. Very high values can crash the game on world entry. [Vanilla: 777]', + 'tweaks.farClip.recommendedHint': 'Recommended for your PC: {value}.', + 'tweaks.frillDistance.label': 'Ground clutter distance', + 'tweaks.frillDistance.text': + 'Changes ground clutter render distance. [Vanilla: 70] [Tweaks: 300]', + 'tweaks.cameraDistance.label': 'Camera distance', + 'tweaks.cameraDistance.text': + 'Increases maximum camera (zoom out) distance. [Vanilla: 50] [Max:100]', + 'tweaks.soundsHeading': 'Sounds', + 'tweaks.soundInBackground.label': 'Background sounds', + 'tweaks.soundInBackground.text': + 'Allows game sounds to play while the game is minimized.', + 'tweaks.applyFailed': 'Failed to apply tweaks: {message}', + 'tweaks.highlighted': 'Highlighted', + 'tweaks.recommendedNote': 'options are recommended and enabled by default', + 'tweaks.reset': 'Reset', + 'tweaks.apply': 'Apply', + 'misc.newsTitle': 'News', + 'misc.announcementsTitle': 'Announcements', + 'misc.patchNotesTitle': 'Patch Notes', + 'misc.newsByAuthor': 'by {author}', + 'misc.newsReadMore': 'Read more', + 'misc.refresh': 'Refresh', + 'misc.newsLoading': 'Loading news...', + 'misc.newsError': "Couldn't reach the news feed.", + 'misc.newsEmpty': 'No news yet. Check back later.', + 'misc.tryAgain': 'Try again', + 'forum.title': 'Nautilus News Network', + 'forum.readFullPost': 'Read full post on the forum', + 'forum.loading': 'Loading latest dispatch...', + 'forum.error': "Couldn't reach the forum.", + 'forum.empty': 'No dispatches yet.', + 'misc.comingSoon': 'Coming soon...', + 'misc.selfUpdateCheckFailed': 'Update check failed: {message}', + 'misc.selfUpdateAvailable': + 'Launcher update {version} available, preparing download…', + 'misc.selfUpdateDownloading': 'Downloading update {version} · {percent}%', + 'misc.selfUpdateReady': 'Launcher update {version} ready to install', + 'misc.selfUpdateInstallNow': 'Install now', + 'misc.tabCrashed': '{tab} crashed', + 'misc.somethingWentWrong': 'Something went wrong!', + 'misc.uncaughtError': 'Uncaught {name}: {message}', + 'misc.unknownError': 'Unknown error', + 'misc.copyError': 'Copy error', + 'misc.reload': 'Reload', + 'mods.update': 'update', + 'mods.ignoreUpdates': 'Ignore updates', + 'mods.title': 'CUSTOM MODS', + 'mods.unsavedChanges': 'unsaved changes', + 'mods.warning': + 'Enabling custom mods may not provide any performance benefits or may even cause game crashes depending on your system. Please try disabling them if you experience any issues.', + 'mods.enableRequired': 'Enable {mods}, required by your selected mods.', + 'mods.depRequired': '{mod} must be enabled. It is required by {requiredBy}.', + 'mods.missingFiles': + 'Missing game files for: {mods}. Re-sync your client to restore them. If antivirus is blocking them, allow them through first.', + 'mods.reverify': 'Re-sync client', + 'mods.yourDlls': 'Your DLL mods', + 'mods.yourDllsEmpty': + 'Drop a .dll into your game folder, or add one below, to toggle it here.', + 'mods.addDll': 'Add DLL', + 'mods.addDllTitle': 'Choose a DLL mod', + 'mods.addDllFailed': 'Failed to add DLL.', + 'mods.highlighted': 'Highlighted', + 'mods.highlightedRecommended': 'mods are recommended.', + 'mods.apply': 'Apply', + 'mods.applied': 'Applied', + 'mods.cantApplyYet': "CAN'T APPLY YET", + 'mods.close': 'Close', + 'av.blockedTitle': 'BLOCKED BY ANTIVIRUS', + 'av.blockedOne': 'Windows Defender blocked this mod:', + 'av.blockedMany': 'Windows Defender blocked these mods:', + 'av.blockedExplain': + 'These are safe community mods that Defender mis-flags. Allow them once, then try again.', + 'av.whyHappening': 'Why is this happening?', + 'av.allowThrough': 'Allow through antivirus', + 'av.addedRetry': 'Added. Close this and try again.', + 'av.close': 'Close', + 'av.whyTitle': 'WHY ANTIVIRUS FLAGS MODS', + 'av.whyIntro': + 'Some of the mods the launcher installs get flagged by Windows Defender (or other antivirus) as a threat such as "{detection}". This is a', + 'av.falsePositive': 'false positive', + 'av.whatSetsItOff': 'What sets it off', + 'av.whatSetsItOffIntro': + 'These mods modernize the 20-year-old 1.12 game client using two techniques that look suspicious to antivirus heuristics:', + 'av.dllInjection': 'DLL injection', + 'av.dllInjectionText': + 'the mods load extra code into the running game to add features (ping compensation, better nameplates, performance). Loading code into another program is also something malware does, so the behavior alone trips the scanner.', + 'av.exePatching': 'Executable patching', + 'av.exePatchingText': + 'some mods edit WoW.exe to lift old engine limits (for example letting the 32-bit client use 4GB of RAM instead of 2GB). Modifying an executable is another malware-like pattern.', + 'av.heuristicNote': + 'Because these are behavior-based (heuristic) detections, not matches against real malware, legitimate mods that use them get caught as generic "trojan" or "hacktool" false positives.', + 'av.whatModsDo': 'What the mods actually do', + 'av.modVanillaFixes': + 'the loader that fixes stutter and timer bugs and starts the other mods.', + 'av.modNampower': + 'ping compensation so casts feel responsive on higher latency.', + 'av.modSuperWowUnitXp': + 'advanced Lua, real nameplates, camera and targeting upgrades.', + 'av.modDxvk': + 'translates the old DirectX 9 to modern Vulkan for big FPS gains.', + 'av.howToVerify': 'How to verify these claims', + 'av.howToVerifyText': + 'These are open-source community projects. You can read every line of each mod\'s code from the source link next to it in the Mods list, and you can look up the detection name (for example "{detection}") to see it is a documented false positive on these tools.', + 'av.whatAllowDoesTitle': 'What does "Allow through antivirus" do', + 'av.whatAllowDoesIntro': + "It asks Windows for administrator permission (one prompt, click Yes), then runs Windows' own built-in", + 'av.whatAllowDoesIntroAfter': + 'command to add a few entries to the Windows Defender exclusion list:', + 'av.exclusionFoldersBefore': 'Your', + 'av.gameFolder': 'game folder', + 'av.exclusionFoldersAnd': 'and the', + 'av.launcherFolder': 'launcher folder', + 'av.exclusionFoldersAfter': + ', so the mod files in them are no longer scanned or quarantined.', + 'av.exclusionExesAnd': 'and', + 'av.exclusionExesAfter': + 'as allowed processes, so they are not blocked while running.', + 'av.whatAllowDoesOutro': + 'This is the same thing you would do by hand under Windows Security, Virus and threat protection, Exclusions. It installs nothing, runs no third-party software, and changes nothing outside those specific paths.', + 'av.back': 'Back', + 'addons.sectionInstalled': 'Installed', + 'addons.sectionRecommended': 'Recommended', + 'addons.sectionAvailable': 'Available', + 'addons.checkForUpdates': 'Check for updates', + 'addons.addCustomGitAddon': 'Add custom git addon', + 'addons.updateAll': 'Update all', + 'addons.everythingUpToDate': 'Everything is up to date.', + 'addons.installAddon': 'Install addon', + 'addons.previewAlt': 'Preview', + 'addons.readyToInstall': 'Ready to install', + 'addons.invalidGitUrl': 'Not a valid git repository URL', + 'addons.install': 'Install', + 'addons.warnIncorrectVersionFull': + 'This addon seems to be made for different game version ({version}) and it may not function correctly', + 'addons.warnIncorrectVersionShort': 'Incorrect version', + 'addons.warnMissingDependenciesFull': + 'This addon has missing dependencies: {deps}', + 'addons.warnMissingDependenciesShort': 'Missing dependencies', + 'addons.details': 'Details', + 'addons.openOnGithub': 'Open {url} on GitHub', + 'addons.upToDate': 'Up to date', + 'addons.notVersioned': 'Not versioned', + 'addons.update': 'Update', + 'addons.download': 'Download', + 'addons.deleteConfirmTitle': 'Are you sure?', + 'addons.deleteConfirmBody': 'Are you sure you want to delete {folder} addon?', + 'addons.deleteConfirmFiles': + 'This will delete all files in the addon folder.', + 'addons.delete': 'Delete', + 'addons.remove': 'Remove', + 'addons.detailSource': 'Source', + 'addons.openOnGithubShort': 'Open on GitHub', + 'addons.detailContributions': 'Contributions', + 'addons.detailAddonVersion': 'Addon version', + 'addons.detailDependencies': 'Dependencies', + 'addons.optional': '(optional)', + 'prefs.mirrorOffline': 'offline', + 'prefs.mirrorChecking': 'checking…', + 'prefs.mirrorOnline': 'online', + 'prefs.title': 'SETTINGS', + 'prefs.installLocation': 'INSTALL LOCATION:', + 'prefs.openFolder': 'Open folder', + 'prefs.notSelected': 'Not selected', + 'prefs.change': 'Change', + 'prefs.downloadMirror': 'DOWNLOAD MIRROR:', + 'prefs.recheck': 'Re-check', + 'prefs.troubleshooting': 'TROUBLESHOOTING:', + 'prefs.verifyGameFiles': 'Verify game files', + 'prefs.openLogFile': 'Open log file', + 'prefs.allowThroughAntivirus': 'Allow through antivirus', + 'prefs.exclusionAdded': 'Added. Re-verify game files if any were removed.', + 'prefs.generalSettings': 'GENERAL SETTINGS:', + 'prefs.cleanWdb': 'Clean WDB on each launch', + 'prefs.minimizeToTray': 'Minimize to tray while playing', + 'prefs.shareDownloads': 'Help share downloads with other players', + 'prefs.save': 'Save', + 'prefs.installLocationTitle': 'Install location', + 'prefs.portableInfo': + 'You are using the portable version of the launcher. Install location is determined by the location of the launcher executable.', + 'prefs.errorLabel': 'Error: ', + 'prefs.wowExeNotFound': + '{exe} not found in current folder. Please close the launcher and move it to your WoW 1.12 client directory.', + 'prefs.selectDirectory': + 'Select a directory for the game client installation.', + 'prefs.upgradeExisting': + 'You may also choose a directory with an existing Turtle WoW or Vanilla WoW installation, and it will be automatically upgraded.', + 'prefs.installDirectory': 'Install directory:', + 'prefs.noClientHere': + 'No {exe} in this folder. The launcher will download a fresh client here, and it will not contain your existing addons or settings. If you already have an install, pick that folder instead.', + 'prefs.noClientHereConfirm': + 'I understand — download a fresh client into this folder', + 'prefs.confirm': 'Confirm' +}; + +const deDE: Dict = { + 'tab.news': 'Neuigkeiten', + 'tab.tweaks': 'Optimierungen', + 'tab.addons': 'Addons', + 'tab.mods': 'Mods', + 'topbar.settings': 'Einstellungen', + 'topbar.minimize': 'Minimieren', + 'topbar.quit': 'Beenden', + 'topbar.language': 'Sprache', + 'quit.title': 'Beenden?', + 'quit.warn': + 'Dein Spiel wird gerade aktualisiert. Ein Beenden kann Probleme verursachen.', + 'quit.return': 'Zurück', + 'launch.verifying': 'Überprüfung', + 'launch.play': 'Spielen', + 'launch.retry': 'Erneut', + 'launch.install': 'Installieren', + 'launch.update': 'Aktualisieren', + 'launch.updating': 'Aktualisierung', + 'launch.applying': 'Anwenden', + 'launch.updateAvailable': 'Update verfügbar!', + 'launch.modsChanged': 'Mods geändert, vor dem Spielen anwenden', + 'launch.upToDate': 'Alles aktuell!', + 'launch.version': 'Version: {version}', + 'launch.errorLabel': 'Fehler:', + 'launch.serverFail': 'Update-Server nicht erreichbar', + 'launch.localVersion': 'Du kannst die lokale Version {version} starten', + 'launch.tryLater': 'Bitte versuche es später erneut', + 'launch.verifyHint': + 'Überprüfe deine Spieldaten, indem du auf „Erneut“ klickst.', + 'launch.remaining': 'verbleibend', + 'launch.calculating': 'wird berechnet…', + 'launch.onDisk': 'auf der Festplatte', + 'tweaks.raidVisuals.label': 'Aktualisierte Raid-Effekte', + 'tweaks.raidVisuals.text': + 'Optionaler Download (~9 MB). Fügt deutlichere Bodenmarkierungen und Sounds für Raidboss-Fähigkeiten hinzu, automatisch mit dem Server synchron gehalten. (auch bekannt als Patch-O)', + 'tweaks.alwaysAutoLoot.label': 'Immer automatisch plündern', + 'tweaks.alwaysAutoLoot.text': + 'Kehrt das Auto-Plündern-Verhalten um, sodass immer automatisch geplündert wird und das Auto-Plündern per Tastenkombination deaktiviert ist.', + 'tweaks.largeAddress.label': 'Large Address Aware', + 'tweaks.largeAddress.text': + 'Erlaubt dem Spiel, mehr als 2 GB Arbeitsspeicher zu nutzen.', + 'tweaks.nameplateRange.label': 'Namensplaketten-Reichweite', + 'tweaks.nameplateRange.text': + 'Erhöht die Entfernung, in der Namensplaketten sichtbar sind. [Vanilla: 20] [Classic: 41]', + 'tweaks.cameraHeading': 'Kamera', + 'tweaks.fieldOfView.label': 'Sichtfeld', + 'tweaks.fieldOfView.text': + 'Empfohlen für Breitbild-Fensterauflösungen. [Vanilla: 90] [Tweaks: 110]', + 'tweaks.farClip.label': 'Sichtweite', + 'tweaks.farClip.text': + 'Erhöht die maximale Sichtweite. Sehr hohe Werte können beim Betreten der Welt zum Absturz führen. [Vanilla: 777]', + 'tweaks.farClip.recommendedHint': 'Empfohlen für deinen PC: {value}.', + 'tweaks.frillDistance.label': 'Bodendetail-Distanz', + 'tweaks.frillDistance.text': + 'Ändert die Renderdistanz für Bodendetails. [Vanilla: 70] [Tweaks: 300]', + 'tweaks.cameraDistance.label': 'Kameradistanz', + 'tweaks.cameraDistance.text': + 'Erhöht die maximale Kameradistanz (Herauszoomen). [Vanilla: 50] [Max:100]', + 'tweaks.soundsHeading': 'Töne', + 'tweaks.soundInBackground.label': 'Hintergrundton', + 'tweaks.soundInBackground.text': + 'Lässt die Spieltöne weiterlaufen, während das Spiel minimiert ist.', + 'tweaks.applyFailed': 'Tweaks konnten nicht angewendet werden: {message}', + 'tweaks.highlighted': 'Hervorgehoben', + 'tweaks.recommendedNote': + 'Optionen werden empfohlen und sind standardmäßig aktiviert', + 'tweaks.reset': 'Zurücksetzen', + 'tweaks.apply': 'Anwenden', + 'mods.update': 'Aktualisieren', + 'mods.ignoreUpdates': 'Updates ignorieren', + 'mods.title': 'BENUTZERDEFINIERTE MODS', + 'mods.unsavedChanges': 'nicht gespeicherte Änderungen', + 'mods.warning': + 'Das Aktivieren benutzerdefinierter Mods bringt je nach System möglicherweise keine Leistungsvorteile oder kann sogar zu Spielabstürzen führen. Bitte deaktiviere sie, falls Probleme auftreten.', + 'mods.enableRequired': + 'Aktiviere {mods}, die von deinen ausgewählten Mods benötigt werden.', + 'mods.depRequired': + '{mod} muss aktiviert sein. Es wird von {requiredBy} benötigt.', + 'mods.highlighted': 'Hervorgehoben', + 'mods.highlightedRecommended': 'Mods werden empfohlen.', + 'mods.apply': 'Anwenden', + 'mods.cantApplyYet': 'NOCH NICHT ANWENDBAR', + 'mods.close': 'Schließen', + 'av.blockedTitle': 'VOM ANTIVIRENPROGRAMM BLOCKIERT', + 'av.blockedOne': 'Windows Defender hat diesen Mod blockiert:', + 'av.blockedMany': 'Windows Defender hat diese Mods blockiert:', + 'av.blockedExplain': + 'Das sind sichere Community-Mods, die der Defender fälschlicherweise meldet. Erlaube sie einmal und versuche es dann erneut.', + 'av.whyHappening': 'Warum passiert das?', + 'av.allowThrough': 'Im Antivirenprogramm zulassen', + 'av.addedRetry': 'Hinzugefügt. Schließe dies und versuche es erneut.', + 'av.close': 'Schließen', + 'av.whyTitle': 'WARUM ANTIVIRENPROGRAMME MODS MELDEN', + 'av.whyIntro': + 'Einige der vom Launcher installierten Mods werden von Windows Defender (oder anderen Antivirenprogrammen) als Bedrohung wie z. B. "{detection}" gemeldet. Dabei handelt es sich um einen', + 'av.falsePositive': 'Fehlalarm', + 'av.whatSetsItOff': 'Was den Alarm auslöst', + 'av.whatSetsItOffIntro': + 'Diese Mods modernisieren den 20 Jahre alten 1.12-Spielclient mit zwei Techniken, die für die Heuristik von Antivirenprogrammen verdächtig aussehen:', + 'av.dllInjection': 'DLL-Injektion', + 'av.dllInjectionText': + 'Die Mods laden zusätzlichen Code in das laufende Spiel, um Funktionen hinzuzufügen (Ping-Kompensation, bessere Namensplaketten, Leistung). Das Laden von Code in ein anderes Programm ist auch etwas, das Schadsoftware tut, daher löst allein dieses Verhalten den Scanner aus.', + 'av.exePatching': 'Patchen der ausführbaren Datei', + 'av.exePatchingText': + 'Einige Mods bearbeiten WoW.exe, um alte Engine-Limits aufzuheben (zum Beispiel, damit der 32-Bit-Client 4 GB RAM statt 2 GB nutzen kann). Das Verändern einer ausführbaren Datei ist ein weiteres schadsoftwareähnliches Muster.', + 'av.heuristicNote': + 'Da es sich um verhaltensbasierte (heuristische) Erkennungen handelt und nicht um Übereinstimmungen mit echter Schadsoftware, werden legitime Mods, die diese Techniken nutzen, fälschlicherweise als allgemeine "Trojaner"- oder "Hacktool"-Bedrohung eingestuft.', + 'av.whatModsDo': 'Was die Mods tatsächlich tun', + 'av.modVanillaFixes': + 'der Loader, der Ruckler und Timer-Fehler behebt und die anderen Mods startet.', + 'av.modNampower': + 'Ping-Kompensation, damit sich Zauber bei höherer Latenz reaktionsschnell anfühlen.', + 'av.modSuperWowUnitXp': + 'erweitertes Lua, echte Namensplaketten, Kamera- und Zielerfassungs-Verbesserungen.', + 'av.modDxvk': + 'übersetzt das alte DirectX 9 in das moderne Vulkan für große FPS-Gewinne.', + 'av.howToVerify': 'Wie du diese Angaben überprüfen kannst', + 'av.howToVerifyText': + 'Das sind quelloffene Community-Projekte. Du kannst jede Zeile des Codes jedes Mods über den Quell-Link daneben in der Mods-Liste einsehen, und du kannst den Erkennungsnamen (zum Beispiel "{detection}") nachschlagen, um zu sehen, dass es sich bei diesen Tools um einen dokumentierten Fehlalarm handelt.', + 'av.whatAllowDoesTitle': 'Was bewirkt "Im Antivirenprogramm zulassen"', + 'av.whatAllowDoesIntro': + 'Es fragt Windows nach Administratorrechten (eine Abfrage, auf Ja klicken) und führt dann den in Windows integrierten', + 'av.whatAllowDoesIntroAfter': + 'Befehl aus, um der Ausschlussliste von Windows Defender einige Einträge hinzuzufügen:', + 'av.exclusionFoldersBefore': 'Deinen', + 'av.gameFolder': 'Spielordner', + 'av.exclusionFoldersAnd': 'und den', + 'av.launcherFolder': 'Launcher-Ordner', + 'av.exclusionFoldersAfter': + ', damit die darin enthaltenen Mod-Dateien nicht mehr gescannt oder in Quarantäne verschoben werden.', + 'av.exclusionExesAnd': 'und', + 'av.exclusionExesAfter': + 'als erlaubte Prozesse, damit sie während der Ausführung nicht blockiert werden.', + 'av.whatAllowDoesOutro': + 'Das ist dasselbe, was du von Hand unter Windows-Sicherheit, Viren- und Bedrohungsschutz, Ausschlüsse tun würdest. Es installiert nichts, führt keine Drittanbietersoftware aus und ändert nichts außerhalb dieser bestimmten Pfade.', + 'av.back': 'Zurück', + 'addons.sectionInstalled': 'Installiert', + 'addons.sectionRecommended': 'Empfohlen', + 'addons.sectionAvailable': 'Verfügbar', + 'addons.checkForUpdates': 'Nach Updates suchen', + 'addons.addCustomGitAddon': 'Benutzerdefiniertes Git-Addon hinzufügen', + 'addons.updateAll': 'Alle aktualisieren', + 'addons.everythingUpToDate': 'Alles ist auf dem neuesten Stand.', + 'addons.installAddon': 'Addon installieren', + 'addons.previewAlt': 'Vorschau', + 'addons.readyToInstall': 'Bereit zur Installation', + 'addons.invalidGitUrl': 'Keine gültige Git-Repository-URL', + 'addons.install': 'Installieren', + 'addons.warnIncorrectVersionFull': + 'Dieses Addon scheint für eine andere Spielversion ({version}) erstellt worden zu sein und funktioniert möglicherweise nicht korrekt', + 'addons.warnIncorrectVersionShort': 'Falsche Version', + 'addons.warnMissingDependenciesFull': + 'Diesem Addon fehlen Abhängigkeiten: {deps}', + 'addons.warnMissingDependenciesShort': 'Fehlende Abhängigkeiten', + 'addons.details': 'Details', + 'addons.openOnGithub': '{url} auf GitHub öffnen', + 'addons.upToDate': 'Aktuell', + 'addons.notVersioned': 'Nicht versioniert', + 'addons.update': 'Aktualisieren', + 'addons.download': 'Herunterladen', + 'addons.deleteConfirmTitle': 'Bist du sicher?', + 'addons.deleteConfirmBody': + 'Möchtest du das Addon {folder} wirklich löschen?', + 'addons.deleteConfirmFiles': + 'Dadurch werden alle Dateien im Addon-Ordner gelöscht.', + 'addons.delete': 'Löschen', + 'addons.remove': 'Entfernen', + 'addons.detailSource': 'Quelle', + 'addons.openOnGithubShort': 'Auf GitHub öffnen', + 'addons.detailContributions': 'Beiträge', + 'addons.detailAddonVersion': 'Addon-Version', + 'addons.detailDependencies': 'Abhängigkeiten', + 'addons.optional': '(optional)', + 'prefs.mirrorOffline': 'offline', + 'prefs.mirrorChecking': 'wird geprüft…', + 'prefs.mirrorOnline': 'online', + 'prefs.title': 'EINSTELLUNGEN', + 'prefs.installLocation': 'INSTALLATIONSORT:', + 'prefs.openFolder': 'Ordner öffnen', + 'prefs.notSelected': 'Nicht ausgewählt', + 'prefs.change': 'Ändern', + 'prefs.downloadMirror': 'DOWNLOAD-MIRROR:', + 'prefs.recheck': 'Erneut prüfen', + 'prefs.troubleshooting': 'FEHLERBEHEBUNG:', + 'prefs.verifyGameFiles': 'Spieldateien überprüfen', + 'prefs.openLogFile': 'Protokolldatei öffnen', + 'prefs.allowThroughAntivirus': 'Im Antivirenprogramm zulassen', + 'prefs.exclusionAdded': + 'Hinzugefügt. Überprüfe die Spieldateien erneut, falls welche entfernt wurden.', + 'prefs.generalSettings': 'ALLGEMEINE EINSTELLUNGEN:', + 'prefs.cleanWdb': 'WDB bei jedem Start leeren', + 'prefs.minimizeToTray': 'Während des Spielens in den Infobereich minimieren', + 'prefs.save': 'Speichern', + 'prefs.installLocationTitle': 'Installationsort', + 'prefs.portableInfo': + 'Du verwendest die portable Version des Launchers. Der Installationsort wird durch den Speicherort der ausführbaren Launcher-Datei bestimmt.', + 'prefs.errorLabel': 'Fehler: ', + 'prefs.wowExeNotFound': + '{exe} wurde im aktuellen Ordner nicht gefunden. Bitte schließe den Launcher und verschiebe ihn in dein WoW-1.12-Client-Verzeichnis.', + 'prefs.selectDirectory': + 'Wähle ein Verzeichnis für die Installation des Spielclients aus.', + 'prefs.upgradeExisting': + 'Du kannst auch ein Verzeichnis mit einer vorhandenen Turtle-WoW- oder Vanilla-WoW-Installation wählen, und es wird automatisch aktualisiert.', + 'prefs.installDirectory': 'Installationsverzeichnis:', + 'prefs.noClientHere': + 'In diesem Ordner ist keine {exe}. Der Launcher lädt hier einen neuen Client herunter, der deine vorhandenen Addons und Einstellungen nicht enthält. Wenn du bereits eine Installation hast, wähle stattdessen deren Ordner.', + 'prefs.noClientHereConfirm': + 'Verstanden — einen neuen Client in diesen Ordner herunterladen', + 'prefs.confirm': 'Bestätigen', + 'misc.newsTitle': 'Neuigkeiten', + 'misc.newsByAuthor': 'von {author}', + 'misc.newsReadMore': 'Mehr lesen', + 'misc.refresh': 'Aktualisieren', + 'misc.newsLoading': 'Neuigkeiten werden geladen...', + 'misc.newsError': 'Der Neuigkeiten-Feed konnte nicht erreicht werden.', + 'misc.newsEmpty': 'Noch keine Neuigkeiten – schau später wieder vorbei.', + 'misc.tryAgain': 'Erneut versuchen', + 'misc.comingSoon': 'Demnächst verfügbar...', + 'misc.selfUpdateCheckFailed': 'Update-Prüfung fehlgeschlagen: {message}', + 'misc.selfUpdateAvailable': + 'Launcher-Update {version} verfügbar – Download wird vorbereitet…', + 'misc.selfUpdateDownloading': + 'Update {version} wird heruntergeladen · {percent}%', + 'misc.selfUpdateReady': 'Launcher-Update {version} bereit zur Installation', + 'misc.selfUpdateInstallNow': 'Jetzt installieren', + 'misc.tabCrashed': '{tab} ist abgestürzt', + 'misc.somethingWentWrong': 'Etwas ist schiefgelaufen!', + 'misc.uncaughtError': 'Nicht abgefangener {name}: {message}', + 'misc.unknownError': 'Unbekannter Fehler', + 'misc.copyError': 'Fehler kopieren', + 'misc.reload': 'Neu laden' +}; + +const zhCN: Dict = { + 'tab.news': '新闻', + 'tab.tweaks': '优化', + 'tab.addons': '插件', + 'tab.mods': '模组', + 'topbar.settings': '设置', + 'topbar.minimize': '最小化', + 'topbar.quit': '退出', + 'topbar.language': '语言', + 'quit.title': '退出?', + 'quit.warn': '游戏正在更新。现在退出可能会导致问题。', + 'quit.return': '返回', + 'launch.verifying': '正在验证', + 'launch.play': '开始游戏', + 'launch.retry': '重试', + 'launch.install': '安装', + 'launch.update': '更新', + 'launch.updating': '正在更新', + 'launch.applying': '正在应用', + 'launch.updateAvailable': '有可用更新!', + 'launch.modsChanged': '模组已更改,请在游戏前应用', + 'launch.upToDate': '一切都是最新的!', + 'launch.version': '版本:{version}', + 'launch.errorLabel': '错误:', + 'launch.serverFail': '无法连接到更新服务器', + 'launch.localVersion': '您可以启动本地版本 {version}', + 'launch.tryLater': '请稍后再试', + 'launch.verifyHint': '点击重试以验证游戏数据。', + 'launch.remaining': '剩余', + 'launch.calculating': '计算中…', + 'launch.onDisk': '在磁盘上', + 'tweaks.raidVisuals.label': '团队副本视觉增强', + 'tweaks.raidVisuals.text': + '可选下载(约 9 MB)。为团队首领技能添加更清晰的地面标记和音效,并自动与服务器保持同步。(又称 Patch-O)', + 'tweaks.alwaysAutoLoot.label': '始终自动拾取', + 'tweaks.alwaysAutoLoot.text': + '反转自动拾取行为,改为始终自动拾取,并禁用按住绑定键的自动拾取。', + 'tweaks.largeAddress.label': '大地址感知', + 'tweaks.largeAddress.text': '允许游戏使用超过 2GB 的内存。', + 'tweaks.nameplateRange.label': '姓名板范围', + 'tweaks.nameplateRange.text': + '增加姓名板可见的距离。[Vanilla: 20] [Classic: 41]', + 'tweaks.cameraHeading': '镜头', + 'tweaks.fieldOfView.label': '视野范围', + 'tweaks.fieldOfView.text': + '推荐用于宽屏窗口分辨率。[Vanilla: 90] [Tweaks: 110]', + 'tweaks.farClip.label': '渲染距离', + 'tweaks.farClip.text': + '增加最大渲染距离。数值过高可能导致进入游戏世界时崩溃。[Vanilla: 777]', + 'tweaks.farClip.recommendedHint': '为您的电脑推荐:{value}。', + 'tweaks.frillDistance.label': '地表杂物距离', + 'tweaks.frillDistance.text': + '更改地表杂物的渲染距离。[Vanilla: 70] [Tweaks: 300]', + 'tweaks.cameraDistance.label': '镜头距离', + 'tweaks.cameraDistance.text': + '增加最大镜头(拉远)距离。[Vanilla: 50] [Max:100]', + 'tweaks.soundsHeading': '声音', + 'tweaks.soundInBackground.label': '后台声音', + 'tweaks.soundInBackground.text': '允许游戏在最小化时继续播放声音。', + 'tweaks.applyFailed': '应用优化失败:{message}', + 'tweaks.highlighted': '已高亮', + 'tweaks.recommendedNote': '选项为推荐项,默认已启用', + 'tweaks.reset': '重置', + 'tweaks.apply': '应用', + 'mods.update': '更新', + 'mods.ignoreUpdates': '忽略更新', + 'mods.title': '自定义 Mods', + 'mods.unsavedChanges': '未保存的更改', + 'mods.warning': + '启用自定义 Mods 不一定能带来性能提升,甚至可能因系统差异导致游戏崩溃。如遇到任何问题,请尝试将其禁用。', + 'mods.enableRequired': '启用 {mods},你所选的 Mods 需要它们。', + 'mods.depRequired': '必须启用 {mod},它是 {requiredBy} 所必需的。', + 'mods.highlighted': '已高亮', + 'mods.highlightedRecommended': 'Mods 为推荐项。', + 'mods.apply': '应用', + 'mods.cantApplyYet': '暂时无法应用', + 'mods.close': '关闭', + 'av.blockedTitle': '被杀毒软件拦截', + 'av.blockedOne': 'Windows Defender 拦截了此 Mod:', + 'av.blockedMany': 'Windows Defender 拦截了以下 Mods:', + 'av.blockedExplain': + '这些是安全的社区 Mods,被 Defender 误报了。允许它们一次,然后重试即可。', + 'av.whyHappening': '为什么会这样?', + 'av.allowThrough': '在杀毒软件中放行', + 'av.addedRetry': '已添加。关闭此窗口并重试。', + 'av.close': '关闭', + 'av.whyTitle': '为什么杀毒软件会标记 Mods', + 'av.whyIntro': + '启动器安装的部分 Mods 会被 Windows Defender(或其他杀毒软件)标记为威胁,例如“{detection}”。这是一个', + 'av.falsePositive': '误报', + 'av.whatSetsItOff': '触发原因', + 'av.whatSetsItOffIntro': + '这些 Mods 使用了两种会让杀毒软件启发式检测觉得可疑的技术,来对这款有 20 年历史的 1.12 游戏客户端进行现代化改造:', + 'av.dllInjection': 'DLL 注入', + 'av.dllInjectionText': + '这些 Mods 会向运行中的游戏加载额外代码以添加功能(延迟补偿、更好的姓名板、性能优化)。向其他程序注入代码也是恶意软件会做的事,因此仅凭这一行为就会触发扫描器。', + 'av.exePatching': '可执行文件修补', + 'av.exePatchingText': + '部分 Mods 会修改 WoW.exe 以解除旧引擎的限制(例如让 32 位客户端可使用 4GB 内存而非 2GB)。修改可执行文件是另一种类似恶意软件的特征。', + 'av.heuristicNote': + '由于这些是基于行为的(启发式)检测,而非与真实恶意软件的特征匹配,因此使用这些技术的正规 Mods 会被误报为通用的“木马”或“黑客工具”。', + 'av.whatModsDo': '这些 Mods 实际做了什么', + 'av.modVanillaFixes': '用于修复卡顿和计时器错误并启动其他 Mods 的加载器。', + 'av.modNampower': '延迟补偿,让你在较高延迟下施法依然跟手。', + 'av.modSuperWowUnitXp': '高级 Lua、真正的姓名板、镜头与目标选取的增强。', + 'av.modDxvk': '将旧的 DirectX 9 转换为现代的 Vulkan,大幅提升 FPS。', + 'av.howToVerify': '如何核实这些说法', + 'av.howToVerifyText': + '这些都是开源的社区项目。你可以通过 Mods 列表中每个 Mod 旁的源代码链接阅读其每一行代码,也可以查询检测名称(例如“{detection}”),即可确认它在这些工具上是已被记录的误报。', + 'av.whatAllowDoesTitle': '“在杀毒软件中放行”会做什么', + 'av.whatAllowDoesIntro': + '它会向 Windows 请求管理员权限(仅一次提示,点击“是”),然后运行 Windows 自带的', + 'av.whatAllowDoesIntroAfter': + '命令,向 Windows Defender 排除列表添加几个条目:', + 'av.exclusionFoldersBefore': '你的', + 'av.gameFolder': '游戏文件夹', + 'av.exclusionFoldersAnd': '以及', + 'av.launcherFolder': '启动器文件夹', + 'av.exclusionFoldersAfter': ',这样其中的 Mod 文件就不会再被扫描或隔离。', + 'av.exclusionExesAnd': '和', + 'av.exclusionExesAfter': '作为允许的进程,使它们在运行时不会被拦截。', + 'av.whatAllowDoesOutro': + '这与你在“Windows 安全中心 → 病毒和威胁防护 → 排除项”中手动操作的效果完全相同。它不安装任何东西,不运行任何第三方软件,也不会更改这些特定路径以外的任何内容。', + 'av.back': '返回', + 'addons.sectionInstalled': '已安装', + 'addons.sectionRecommended': '推荐', + 'addons.sectionAvailable': '可用', + 'addons.checkForUpdates': '检查更新', + 'addons.addCustomGitAddon': '添加自定义 git 插件', + 'addons.updateAll': '全部更新', + 'addons.everythingUpToDate': '一切均为最新。', + 'addons.installAddon': '安装插件', + 'addons.previewAlt': '预览', + 'addons.readyToInstall': '准备安装', + 'addons.invalidGitUrl': '不是有效的 git 仓库地址', + 'addons.install': '安装', + 'addons.warnIncorrectVersionFull': + '此插件似乎是为其他游戏版本({version})制作的,可能无法正常运行', + 'addons.warnIncorrectVersionShort': '版本不符', + 'addons.warnMissingDependenciesFull': '此插件缺少依赖项:{deps}', + 'addons.warnMissingDependenciesShort': '缺少依赖项', + 'addons.details': '详情', + 'addons.openOnGithub': '在 GitHub 上打开 {url}', + 'addons.upToDate': '已是最新', + 'addons.notVersioned': '无版本信息', + 'addons.update': '更新', + 'addons.download': '下载', + 'addons.deleteConfirmTitle': '确定吗?', + 'addons.deleteConfirmBody': '确定要删除 {folder} 插件吗?', + 'addons.deleteConfirmFiles': '这将删除该插件文件夹中的所有文件。', + 'addons.delete': '删除', + 'addons.remove': '移除', + 'addons.detailSource': '源代码', + 'addons.openOnGithubShort': '在 GitHub 上打开', + 'addons.detailContributions': '贡献者', + 'addons.detailAddonVersion': '插件版本', + 'addons.detailDependencies': '依赖项', + 'addons.optional': '(可选)', + 'prefs.mirrorOffline': '离线', + 'prefs.mirrorChecking': '检查中…', + 'prefs.mirrorOnline': '在线', + 'prefs.title': '设置', + 'prefs.installLocation': '安装位置:', + 'prefs.openFolder': '打开文件夹', + 'prefs.notSelected': '未选择', + 'prefs.change': '更改', + 'prefs.downloadMirror': '下载镜像:', + 'prefs.recheck': '重新检查', + 'prefs.troubleshooting': '故障排除:', + 'prefs.verifyGameFiles': '校验游戏文件', + 'prefs.openLogFile': '打开日志文件', + 'prefs.allowThroughAntivirus': '在杀毒软件中放行', + 'prefs.exclusionAdded': '已添加。如有文件被删除,请重新校验游戏文件。', + 'prefs.generalSettings': '常规设置:', + 'prefs.cleanWdb': '每次启动时清理 WDB', + 'prefs.minimizeToTray': '游戏时最小化到托盘', + 'prefs.save': '保存', + 'prefs.installLocationTitle': '安装位置', + 'prefs.portableInfo': + '你正在使用便携版启动器。安装位置由启动器可执行文件所在的位置决定。', + 'prefs.errorLabel': '错误:', + 'prefs.wowExeNotFound': + '在当前文件夹中未找到 {exe}。请关闭启动器并将其移动到你的 WoW 1.12 客户端目录。', + 'prefs.selectDirectory': '请选择游戏客户端的安装目录。', + 'prefs.upgradeExisting': + '你也可以选择一个已有 Turtle WoW 或 Vanilla WoW 安装的目录,它将被自动升级。', + 'prefs.installDirectory': '安装目录:', + 'prefs.noClientHere': + '此文件夹中没有 {exe}。启动器将在此处下载全新的客户端,其中不会包含你现有的插件和设置。如果你已经安装过,请改为选择原有的安装目录。', + 'prefs.noClientHereConfirm': '我已了解——在此文件夹下载全新客户端', + 'prefs.confirm': '确认', + 'misc.newsTitle': '新闻', + 'misc.newsByAuthor': '作者:{author}', + 'misc.newsReadMore': '阅读更多', + 'misc.refresh': '刷新', + 'misc.newsLoading': '正在加载新闻...', + 'misc.newsError': '无法连接到新闻源。', + 'misc.newsEmpty': '暂无新闻,请稍后再来查看。', + 'misc.tryAgain': '重试', + 'misc.comingSoon': '敬请期待...', + 'misc.selfUpdateCheckFailed': '检查更新失败:{message}', + 'misc.selfUpdateAvailable': '启动器更新 {version} 可用,正在准备下载…', + 'misc.selfUpdateDownloading': '正在下载更新 {version} · {percent}%', + 'misc.selfUpdateReady': '启动器更新 {version} 已准备好安装', + 'misc.selfUpdateInstallNow': '立即安装', + 'misc.tabCrashed': '{tab} 已崩溃', + 'misc.somethingWentWrong': '出错了!', + 'misc.uncaughtError': '未捕获的 {name}:{message}', + 'misc.unknownError': '未知错误', + 'misc.copyError': '复制错误信息', + 'misc.reload': '重新加载' +}; + +const esES: Dict = { + 'tab.news': 'Noticias', + 'tab.tweaks': 'Ajustes', + 'tab.addons': 'Addons', + 'tab.mods': 'Mods', + 'topbar.settings': 'Configuración', + 'topbar.minimize': 'Minimizar', + 'topbar.quit': 'Salir', + 'topbar.language': 'Idioma', + 'quit.title': '¿Salir?', + 'quit.warn': + 'Tu juego se está actualizando. Salir ahora puede causar problemas.', + 'quit.return': 'Volver', + 'launch.verifying': 'Verificando', + 'launch.play': 'Jugar', + 'launch.retry': 'Reintentar', + 'launch.install': 'Instalar', + 'launch.update': 'Actualizar', + 'launch.updating': 'Actualizando', + 'launch.applying': 'Aplicando', + 'launch.updateAvailable': '¡Actualización disponible!', + 'launch.modsChanged': 'Mods cambiados, aplícalos antes de jugar', + 'launch.upToDate': '¡Todo está actualizado!', + 'launch.version': 'Versión: {version}', + 'launch.errorLabel': 'Error:', + 'launch.serverFail': 'No se pudo conectar al servidor de actualización', + 'launch.localVersion': 'Puedes iniciar la versión local {version}', + 'launch.tryLater': 'Inténtalo de nuevo más tarde', + 'launch.verifyHint': 'Verifica los datos del juego pulsando Reintentar.', + 'launch.remaining': 'restante', + 'launch.calculating': 'calculando…', + 'launch.onDisk': 'en disco', + 'tweaks.raidVisuals.label': 'Efectos de banda mejorados', + 'tweaks.raidVisuals.text': + 'Descarga opcional de ~9 MB. Añade marcadores de suelo y sonidos más claros para las habilidades de los jefes de banda, sincronizados automáticamente con el servidor. (también conocido como Patch-O)', + 'tweaks.alwaysAutoLoot.label': 'Saqueo automático siempre', + 'tweaks.alwaysAutoLoot.text': + 'Invierte el comportamiento del saqueo automático para saquear siempre de forma automática y desactivar el saqueo automático con tecla asignada.', + 'tweaks.largeAddress.label': 'Large Address Aware', + 'tweaks.largeAddress.text': + 'Permite que el juego utilice más de 2 GB de memoria.', + 'tweaks.nameplateRange.label': 'Alcance de las placas de nombre', + 'tweaks.nameplateRange.text': + 'Aumenta la distancia a la que son visibles las placas de nombre. [Vanilla: 20] [Classic: 41]', + 'tweaks.cameraHeading': 'Cámara', + 'tweaks.fieldOfView.label': 'Campo de visión', + 'tweaks.fieldOfView.text': + 'Recomendado para resoluciones de ventana panorámicas. [Vanilla: 90] [Tweaks: 110]', + 'tweaks.farClip.label': 'Distancia de renderizado', + 'tweaks.farClip.text': + 'Aumenta la distancia máxima de renderizado. Los valores muy altos pueden bloquear el juego al entrar al mundo. [Vanilla: 777]', + 'tweaks.farClip.recommendedHint': 'Recomendado para tu PC: {value}.', + 'tweaks.frillDistance.label': 'Distancia de la maleza', + 'tweaks.frillDistance.text': + 'Cambia la distancia de renderizado de la maleza del suelo. [Vanilla: 70] [Tweaks: 300]', + 'tweaks.cameraDistance.label': 'Distancia de la cámara', + 'tweaks.cameraDistance.text': + 'Aumenta la distancia máxima de la cámara (alejar el zoom). [Vanilla: 50] [Max:100]', + 'tweaks.soundsHeading': 'Sonidos', + 'tweaks.soundInBackground.label': 'Sonidos en segundo plano', + 'tweaks.soundInBackground.text': + 'Permite que el sonido del juego se reproduzca mientras está minimizado.', + 'tweaks.applyFailed': 'No se han podido aplicar los ajustes: {message}', + 'tweaks.highlighted': 'Destacado', + 'tweaks.recommendedNote': + 'opciones recomendadas y activadas de forma predeterminada', + 'tweaks.reset': 'Restablecer', + 'tweaks.apply': 'Aplicar', + 'mods.update': 'actualizar', + 'mods.ignoreUpdates': 'Ignorar actualizaciones', + 'mods.title': 'MODS PERSONALIZADOS', + 'mods.unsavedChanges': 'cambios sin guardar', + 'mods.warning': + 'Activar los mods personalizados puede no aportar ninguna mejora de rendimiento o incluso provocar cierres inesperados del juego según tu sistema. Prueba a desactivarlos si experimentas algún problema.', + 'mods.enableRequired': + 'Activa {mods}, necesarios para los mods seleccionados.', + 'mods.depRequired': + '{mod} debe estar activado. Es necesario para {requiredBy}.', + 'mods.highlighted': 'Destacado', + 'mods.highlightedRecommended': 'mods recomendados.', + 'mods.apply': 'Aplicar', + 'mods.cantApplyYet': 'AÚN NO SE PUEDE APLICAR', + 'mods.close': 'Cerrar', + 'av.blockedTitle': 'BLOQUEADO POR EL ANTIVIRUS', + 'av.blockedOne': 'Windows Defender ha bloqueado este mod:', + 'av.blockedMany': 'Windows Defender ha bloqueado estos mods:', + 'av.blockedExplain': + 'Son mods seguros de la comunidad que Defender marca por error. Permítelos una vez y vuelve a intentarlo.', + 'av.whyHappening': '¿Por qué ocurre esto?', + 'av.allowThrough': 'Permitir a través del antivirus', + 'av.addedRetry': 'Añadido. Cierra esta ventana y vuelve a intentarlo.', + 'av.close': 'Cerrar', + 'av.whyTitle': 'POR QUÉ EL ANTIVIRUS MARCA LOS MODS', + 'av.whyIntro': + 'Algunos de los mods que instala el launcher son marcados por Windows Defender (u otro antivirus) como una amenaza, por ejemplo «{detection}». Se trata de un', + 'av.falsePositive': 'falso positivo', + 'av.whatSetsItOff': 'Qué lo provoca', + 'av.whatSetsItOffIntro': + 'Estos mods modernizan el cliente del juego 1.12, de hace 20 años, mediante dos técnicas que resultan sospechosas para la heurística del antivirus:', + 'av.dllInjection': 'Inyección de DLL', + 'av.dllInjectionText': + 'los mods cargan código adicional en el juego en ejecución para añadir funciones (compensación de ping, mejores placas de nombre, rendimiento). Cargar código en otro programa es también algo que hace el malware, así que solo ese comportamiento ya activa el análisis.', + 'av.exePatching': 'Parcheo del ejecutable', + 'av.exePatchingText': + 'algunos mods editan WoW.exe para eliminar los límites del viejo motor (por ejemplo, permitir que el cliente de 32 bits use 4 GB de RAM en vez de 2 GB). Modificar un ejecutable es otro patrón propio del malware.', + 'av.heuristicNote': + 'Como son detecciones basadas en el comportamiento (heurísticas) y no coincidencias con malware real, los mods legítimos que las utilizan se detectan como falsos positivos genéricos de tipo «trojan» o «hacktool».', + 'av.whatModsDo': 'Qué hacen realmente los mods', + 'av.modVanillaFixes': + 'el cargador que corrige los tirones y los errores de temporización e inicia los demás mods.', + 'av.modNampower': + 'compensación de ping para que los lanzamientos respondan bien con latencias más altas.', + 'av.modSuperWowUnitXp': + 'Lua avanzado, placas de nombre reales y mejoras de cámara y selección de objetivos.', + 'av.modDxvk': + 'traduce el antiguo DirectX 9 al moderno Vulkan para obtener grandes mejoras de FPS.', + 'av.howToVerify': 'Cómo verificar estas afirmaciones', + 'av.howToVerifyText': + 'Son proyectos de código abierto de la comunidad. Puedes leer cada línea del código de cada mod desde el enlace al código fuente que aparece junto a él en la lista de Mods, y puedes buscar el nombre de la detección (por ejemplo «{detection}») para comprobar que es un falso positivo documentado en estas herramientas.', + 'av.whatAllowDoesTitle': 'Qué hace «Permitir a través del antivirus»', + 'av.whatAllowDoesIntro': + 'Pide permiso de administrador a Windows (un único aviso, haz clic en Sí) y luego ejecuta el propio comando integrado de Windows', + 'av.whatAllowDoesIntroAfter': + 'para añadir unas pocas entradas a la lista de exclusiones de Windows Defender:', + 'av.exclusionFoldersBefore': 'Tu', + 'av.gameFolder': 'carpeta del juego', + 'av.exclusionFoldersAnd': 'y la', + 'av.launcherFolder': 'carpeta del launcher', + 'av.exclusionFoldersAfter': + ', de modo que los archivos de los mods que contienen dejan de analizarse y de ponerse en cuarentena.', + 'av.exclusionExesAnd': 'y', + 'av.exclusionExesAfter': + 'como procesos permitidos, para que no se bloqueen durante su ejecución.', + 'av.whatAllowDoesOutro': + 'Es lo mismo que harías a mano en Seguridad de Windows, Protección antivirus y contra amenazas, Exclusiones. No instala nada, no ejecuta software de terceros y no cambia nada fuera de esas rutas concretas.', + 'av.back': 'Atrás', + 'addons.sectionInstalled': 'Instalados', + 'addons.sectionRecommended': 'Recomendados', + 'addons.sectionAvailable': 'Disponibles', + 'addons.checkForUpdates': 'Buscar actualizaciones', + 'addons.addCustomGitAddon': 'Añadir addon de git personalizado', + 'addons.updateAll': 'Actualizar todo', + 'addons.everythingUpToDate': 'Todo está actualizado.', + 'addons.installAddon': 'Instalar addon', + 'addons.previewAlt': 'Vista previa', + 'addons.readyToInstall': 'Listo para instalar', + 'addons.invalidGitUrl': 'No es una URL de repositorio git válida', + 'addons.install': 'Instalar', + 'addons.warnIncorrectVersionFull': + 'Este addon parece estar hecho para una versión distinta del juego ({version}) y puede que no funcione correctamente', + 'addons.warnIncorrectVersionShort': 'Versión incorrecta', + 'addons.warnMissingDependenciesFull': + 'A este addon le faltan dependencias: {deps}', + 'addons.warnMissingDependenciesShort': 'Faltan dependencias', + 'addons.details': 'Detalles', + 'addons.openOnGithub': 'Abrir {url} en GitHub', + 'addons.upToDate': 'Actualizado', + 'addons.notVersioned': 'Sin versión', + 'addons.update': 'Actualizar', + 'addons.download': 'Descargar', + 'addons.deleteConfirmTitle': '¿Estás seguro?', + 'addons.deleteConfirmBody': '¿Seguro que quieres eliminar el addon {folder}?', + 'addons.deleteConfirmFiles': + 'Esto eliminará todos los archivos de la carpeta del addon.', + 'addons.delete': 'Eliminar', + 'addons.remove': 'Quitar', + 'addons.detailSource': 'Código fuente', + 'addons.openOnGithubShort': 'Abrir en GitHub', + 'addons.detailContributions': 'Contribuciones', + 'addons.detailAddonVersion': 'Versión del addon', + 'addons.detailDependencies': 'Dependencias', + 'addons.optional': '(opcional)', + 'prefs.mirrorOffline': 'sin conexión', + 'prefs.mirrorChecking': 'comprobando…', + 'prefs.mirrorOnline': 'en línea', + 'prefs.title': 'AJUSTES', + 'prefs.installLocation': 'UBICACIÓN DE INSTALACIÓN:', + 'prefs.openFolder': 'Abrir carpeta', + 'prefs.notSelected': 'Sin seleccionar', + 'prefs.change': 'Cambiar', + 'prefs.downloadMirror': 'SERVIDOR DE DESCARGA:', + 'prefs.recheck': 'Volver a comprobar', + 'prefs.troubleshooting': 'SOLUCIÓN DE PROBLEMAS:', + 'prefs.verifyGameFiles': 'Verificar archivos del juego', + 'prefs.openLogFile': 'Abrir archivo de registro', + 'prefs.allowThroughAntivirus': 'Permitir a través del antivirus', + 'prefs.exclusionAdded': + 'Añadido. Vuelve a verificar los archivos del juego si se eliminó alguno.', + 'prefs.generalSettings': 'AJUSTES GENERALES:', + 'prefs.cleanWdb': 'Limpiar WDB en cada inicio', + 'prefs.minimizeToTray': 'Minimizar a la bandeja mientras juegas', + 'prefs.save': 'Guardar', + 'prefs.installLocationTitle': 'Ubicación de instalación', + 'prefs.portableInfo': + 'Estás usando la versión portátil del launcher. La ubicación de instalación viene determinada por la ubicación del ejecutable del launcher.', + 'prefs.errorLabel': 'Error: ', + 'prefs.wowExeNotFound': + 'No se ha encontrado {exe} en la carpeta actual. Cierra el launcher y muévelo al directorio de tu cliente de WoW 1.12.', + 'prefs.selectDirectory': + 'Selecciona un directorio para la instalación del cliente del juego.', + 'prefs.upgradeExisting': + 'También puedes elegir un directorio con una instalación existente de Turtle WoW o Vanilla WoW, y se actualizará automáticamente.', + 'prefs.installDirectory': 'Directorio de instalación:', + 'prefs.noClientHere': + 'No hay ningún {exe} en esta carpeta. El launcher descargará aquí un cliente nuevo, que no incluirá tus addons ni tu configuración actuales. Si ya tienes una instalación, elige esa carpeta.', + 'prefs.noClientHereConfirm': + 'Lo entiendo: descargar un cliente nuevo en esta carpeta', + 'prefs.confirm': 'Confirmar', + 'misc.newsTitle': 'Noticias', + 'misc.newsByAuthor': 'por {author}', + 'misc.newsReadMore': 'Leer más', + 'misc.refresh': 'Actualizar', + 'misc.newsLoading': 'Cargando noticias...', + 'misc.newsError': 'No se ha podido acceder al feed de noticias.', + 'misc.newsEmpty': 'Aún no hay noticias. Vuelve más tarde.', + 'misc.tryAgain': 'Reintentar', + 'misc.comingSoon': 'Próximamente...', + 'misc.selfUpdateCheckFailed': 'Error al buscar actualizaciones: {message}', + 'misc.selfUpdateAvailable': + 'Actualización del launcher {version} disponible. Preparando la descarga…', + 'misc.selfUpdateDownloading': + 'Descargando la actualización {version} · {percent}%', + 'misc.selfUpdateReady': + 'Actualización del launcher {version} lista para instalar', + 'misc.selfUpdateInstallNow': 'Instalar ahora', + 'misc.tabCrashed': '{tab} ha fallado', + 'misc.somethingWentWrong': '¡Algo ha salido mal!', + 'misc.uncaughtError': '{name} no capturado: {message}', + 'misc.unknownError': 'Error desconocido', + 'misc.copyError': 'Copiar error', + 'misc.reload': 'Recargar' +}; + +const ptBR: Dict = { + 'tab.news': 'Notícias', + 'tab.tweaks': 'Ajustes', + 'tab.addons': 'Addons', + 'tab.mods': 'Mods', + 'topbar.settings': 'Configurações', + 'topbar.minimize': 'Minimizar', + 'topbar.quit': 'Sair', + 'topbar.language': 'Idioma', + 'quit.title': 'Sair?', + 'quit.warn': + 'Seu jogo está sendo atualizado. Sair agora pode causar problemas.', + 'quit.return': 'Voltar', + 'launch.verifying': 'Verificando', + 'launch.play': 'Jogar', + 'launch.retry': 'Tentar de novo', + 'launch.install': 'Instalar', + 'launch.update': 'Atualizar', + 'launch.updating': 'Atualizando', + 'launch.applying': 'Aplicando', + 'launch.updateAvailable': 'Atualização disponível!', + 'launch.modsChanged': 'Mods alterados, aplique antes de jogar', + 'launch.upToDate': 'Tudo atualizado!', + 'launch.version': 'Versão: {version}', + 'launch.errorLabel': 'Erro:', + 'launch.serverFail': 'Falha ao conectar ao servidor de atualização', + 'launch.localVersion': 'Você pode iniciar a versão local {version}', + 'launch.tryLater': 'Tente novamente mais tarde', + 'launch.verifyHint': 'Verifique os dados do jogo clicando em Tentar de novo.', + 'launch.remaining': 'restante', + 'launch.calculating': 'calculando…', + 'launch.onDisk': 'no disco', + 'tweaks.raidVisuals.label': 'Efeitos de raide atualizados', + 'tweaks.raidVisuals.text': + 'Download opcional de ~9 MB. Adiciona marcações de chão e sons mais claros para as habilidades dos chefes de raide, mantidos em sincronia com o servidor automaticamente. (também conhecido como Patch-O)', + 'tweaks.alwaysAutoLoot.label': 'Saque automático sempre ativo', + 'tweaks.alwaysAutoLoot.text': + 'Inverte o comportamento do saque automático para saquear sempre automaticamente e desativa o saque automático com a tecla atribuída.', + 'tweaks.largeAddress.label': 'Large Address Aware', + 'tweaks.largeAddress.text': 'Permite que o jogo use mais de 2GB de memória.', + 'tweaks.nameplateRange.label': 'Alcance das placas de nome', + 'tweaks.nameplateRange.text': + 'Aumenta a distância em que as placas de nome ficam visíveis. [Vanilla: 20] [Classic: 41]', + 'tweaks.cameraHeading': 'Câmera', + 'tweaks.fieldOfView.label': 'Campo de visão', + 'tweaks.fieldOfView.text': + 'Recomendado para resoluções de janela widescreen. [Vanilla: 90] [Tweaks: 110]', + 'tweaks.farClip.label': 'Distância de renderização', + 'tweaks.farClip.text': + 'Aumenta a distância máxima de renderização. Valores muito altos podem travar o jogo ao entrar no mundo. [Vanilla: 777]', + 'tweaks.farClip.recommendedHint': 'Recomendado para o seu PC: {value}.', + 'tweaks.frillDistance.label': 'Distância da vegetação do solo', + 'tweaks.frillDistance.text': + 'Altera a distância de renderização da vegetação do solo. [Vanilla: 70] [Tweaks: 300]', + 'tweaks.cameraDistance.label': 'Distância da câmera', + 'tweaks.cameraDistance.text': + 'Aumenta a distância máxima da câmera (zoom para fora). [Vanilla: 50] [Max:100]', + 'tweaks.soundsHeading': 'Sons', + 'tweaks.soundInBackground.label': 'Sons em segundo plano', + 'tweaks.soundInBackground.text': + 'Permite que os sons do jogo continuem tocando enquanto o jogo está minimizado.', + 'tweaks.applyFailed': 'Falha ao aplicar os ajustes: {message}', + 'tweaks.highlighted': 'Destacadas', + 'tweaks.recommendedNote': 'opções são recomendadas e ativadas por padrão', + 'tweaks.reset': 'Redefinir', + 'tweaks.apply': 'Aplicar', + 'mods.update': 'atualizar', + 'mods.ignoreUpdates': 'Ignorar atualizações', + 'mods.title': 'MODS PERSONALIZADOS', + 'mods.unsavedChanges': 'alterações não salvas', + 'mods.warning': + 'Ativar mods personalizados pode não trazer nenhum ganho de desempenho ou até causar travamentos no jogo, dependendo do seu sistema. Tente desativá-los caso enfrente algum problema.', + 'mods.enableRequired': + 'Ative {mods}, exigidos pelos mods que você selecionou.', + 'mods.depRequired': + '{mod} precisa estar ativado. Ele é exigido por {requiredBy}.', + 'mods.highlighted': 'Destacados', + 'mods.highlightedRecommended': 'mods são recomendados.', + 'mods.apply': 'Aplicar', + 'mods.cantApplyYet': 'AINDA NÃO É POSSÍVEL APLICAR', + 'mods.close': 'Fechar', + 'av.blockedTitle': 'BLOQUEADO PELO ANTIVÍRUS', + 'av.blockedOne': 'O Windows Defender bloqueou este mod:', + 'av.blockedMany': 'O Windows Defender bloqueou estes mods:', + 'av.blockedExplain': + 'Estes são mods seguros da comunidade que o Defender sinaliza por engano. Permita-os uma vez e tente novamente.', + 'av.whyHappening': 'Por que isso está acontecendo?', + 'av.allowThrough': 'Permitir no antivírus', + 'av.addedRetry': 'Adicionado. Feche isto e tente novamente.', + 'av.close': 'Fechar', + 'av.whyTitle': 'POR QUE O ANTIVÍRUS SINALIZA OS MODS', + 'av.whyIntro': + 'Alguns dos mods que o launcher instala são sinalizados pelo Windows Defender (ou outro antivírus) como uma ameaça, por exemplo "{detection}". Isso é um', + 'av.falsePositive': 'falso positivo', + 'av.whatSetsItOff': 'O que dispara o alerta', + 'av.whatSetsItOffIntro': + 'Estes mods modernizam o cliente do jogo 1.12, que tem 20 anos, usando duas técnicas que parecem suspeitas para as heurísticas de antivírus:', + 'av.dllInjection': 'Injeção de DLL', + 'av.dllInjectionText': + 'os mods carregam código extra no jogo em execução para adicionar recursos (compensação de ping, melhores placas de nome, desempenho). Carregar código em outro programa também é algo que malwares fazem, então só esse comportamento já aciona o scanner.', + 'av.exePatching': 'Modificação de executável', + 'av.exePatchingText': + 'alguns mods editam o WoW.exe para remover limites antigos do motor (por exemplo, permitir que o cliente de 32 bits use 4GB de RAM em vez de 2GB). Modificar um executável é outro padrão típico de malware.', + 'av.heuristicNote': + 'Como são detecções baseadas em comportamento (heurísticas), e não correspondências com malware real, mods legítimos que usam essas técnicas acabam sendo pegos como falsos positivos genéricos de "trojan" ou "hacktool".', + 'av.whatModsDo': 'O que os mods realmente fazem', + 'av.modVanillaFixes': + 'o carregador que corrige travamentos e bugs de timer e inicia os outros mods.', + 'av.modNampower': + 'compensação de ping para que as conjurações fiquem responsivas em latências mais altas.', + 'av.modSuperWowUnitXp': + 'Lua avançado, placas de nome reais e melhorias de câmera e seleção de alvo.', + 'av.modDxvk': + 'traduz o antigo DirectX 9 para o moderno Vulkan, com grandes ganhos de FPS.', + 'av.howToVerify': 'Como verificar essas afirmações', + 'av.howToVerifyText': + 'Estes são projetos de código aberto da comunidade. Você pode ler cada linha do código de cada mod pelo link da fonte ao lado dele na lista de Mods, e pode pesquisar o nome da detecção (por exemplo "{detection}") para ver que é um falso positivo documentado nessas ferramentas.', + 'av.whatAllowDoesTitle': 'O que faz a opção "Permitir no antivírus"', + 'av.whatAllowDoesIntro': + 'Ela pede permissão de administrador ao Windows (um único aviso, clique em Sim) e então executa o próprio comando interno do Windows', + 'av.whatAllowDoesIntroAfter': + 'para adicionar algumas entradas à lista de exclusões do Windows Defender:', + 'av.exclusionFoldersBefore': 'Sua', + 'av.gameFolder': 'pasta do jogo', + 'av.exclusionFoldersAnd': 'e a', + 'av.launcherFolder': 'pasta do launcher', + 'av.exclusionFoldersAfter': + ', para que os arquivos de mod dentro delas não sejam mais escaneados nem colocados em quarentena.', + 'av.exclusionExesAnd': 'e', + 'av.exclusionExesAfter': + 'como processos permitidos, para que não sejam bloqueados durante a execução.', + 'av.whatAllowDoesOutro': + 'Isto é exatamente o que você faria manualmente em Segurança do Windows, Proteção contra vírus e ameaças, Exclusões. Não instala nada, não executa nenhum software de terceiros e não altera nada fora desses caminhos específicos.', + 'av.back': 'Voltar', + 'addons.sectionInstalled': 'Instalados', + 'addons.sectionRecommended': 'Recomendados', + 'addons.sectionAvailable': 'Disponíveis', + 'addons.checkForUpdates': 'Procurar atualizações', + 'addons.addCustomGitAddon': 'Adicionar addon git personalizado', + 'addons.updateAll': 'Atualizar todos', + 'addons.everythingUpToDate': 'Tudo está atualizado.', + 'addons.installAddon': 'Instalar addon', + 'addons.previewAlt': 'Prévia', + 'addons.readyToInstall': 'Pronto para instalar', + 'addons.invalidGitUrl': 'URL de repositório git inválida', + 'addons.install': 'Instalar', + 'addons.warnIncorrectVersionFull': + 'Este addon parece ter sido feito para outra versão do jogo ({version}) e pode não funcionar corretamente', + 'addons.warnIncorrectVersionShort': 'Versão incorreta', + 'addons.warnMissingDependenciesFull': + 'Este addon tem dependências ausentes: {deps}', + 'addons.warnMissingDependenciesShort': 'Dependências ausentes', + 'addons.details': 'Detalhes', + 'addons.openOnGithub': 'Abrir {url} no GitHub', + 'addons.upToDate': 'Atualizado', + 'addons.notVersioned': 'Sem versionamento', + 'addons.update': 'Atualizar', + 'addons.download': 'Baixar', + 'addons.deleteConfirmTitle': 'Tem certeza?', + 'addons.deleteConfirmBody': + 'Tem certeza de que deseja excluir o addon {folder}?', + 'addons.deleteConfirmFiles': + 'Isso excluirá todos os arquivos da pasta do addon.', + 'addons.delete': 'Excluir', + 'addons.remove': 'Remover', + 'addons.detailSource': 'Fonte', + 'addons.openOnGithubShort': 'Abrir no GitHub', + 'addons.detailContributions': 'Contribuições', + 'addons.detailAddonVersion': 'Versão do addon', + 'addons.detailDependencies': 'Dependências', + 'addons.optional': '(opcional)', + 'prefs.mirrorOffline': 'offline', + 'prefs.mirrorChecking': 'verificando…', + 'prefs.mirrorOnline': 'online', + 'prefs.title': 'CONFIGURAÇÕES', + 'prefs.installLocation': 'LOCAL DE INSTALAÇÃO:', + 'prefs.openFolder': 'Abrir pasta', + 'prefs.notSelected': 'Não selecionado', + 'prefs.change': 'Alterar', + 'prefs.downloadMirror': 'ESPELHO DE DOWNLOAD:', + 'prefs.recheck': 'Verificar novamente', + 'prefs.troubleshooting': 'SOLUÇÃO DE PROBLEMAS:', + 'prefs.verifyGameFiles': 'Verificar arquivos do jogo', + 'prefs.openLogFile': 'Abrir arquivo de log', + 'prefs.allowThroughAntivirus': 'Permitir no antivírus', + 'prefs.exclusionAdded': + 'Adicionado. Verifique os arquivos do jogo novamente se algum tiver sido removido.', + 'prefs.generalSettings': 'CONFIGURAÇÕES GERAIS:', + 'prefs.cleanWdb': 'Limpar WDB a cada inicialização', + 'prefs.minimizeToTray': 'Minimizar para a bandeja durante o jogo', + 'prefs.save': 'Salvar', + 'prefs.installLocationTitle': 'Local de instalação', + 'prefs.portableInfo': + 'Você está usando a versão portátil do launcher. O local de instalação é definido pela localização do executável do launcher.', + 'prefs.errorLabel': 'Erro: ', + 'prefs.wowExeNotFound': + '{exe} não encontrado na pasta atual. Feche o launcher e mova-o para o diretório do seu cliente do WoW 1.12.', + 'prefs.selectDirectory': + 'Selecione um diretório para a instalação do cliente do jogo.', + 'prefs.upgradeExisting': + 'Você também pode escolher um diretório com uma instalação existente do Turtle WoW ou Vanilla WoW, que será atualizada automaticamente.', + 'prefs.installDirectory': 'Diretório de instalação:', + 'prefs.noClientHere': + 'Não há {exe} nesta pasta. O launcher vai baixar um cliente novo aqui, sem os seus addons e configurações atuais. Se você já tem uma instalação, escolha a pasta dela.', + 'prefs.noClientHereConfirm': 'Entendi — baixar um cliente novo nesta pasta', + 'prefs.confirm': 'Confirmar', + 'misc.newsTitle': 'Notícias', + 'misc.newsByAuthor': 'por {author}', + 'misc.newsReadMore': 'Ler mais', + 'misc.refresh': 'Atualizar', + 'misc.newsLoading': 'Carregando notícias...', + 'misc.newsError': 'Não foi possível acessar o feed de notícias.', + 'misc.newsEmpty': 'Ainda não há notícias — volte mais tarde.', + 'misc.tryAgain': 'Tentar novamente', + 'misc.comingSoon': 'Em breve...', + 'misc.selfUpdateCheckFailed': 'Falha ao verificar atualizações: {message}', + 'misc.selfUpdateAvailable': + 'Atualização {version} do launcher disponível — preparando o download…', + 'misc.selfUpdateDownloading': 'Baixando a atualização {version} · {percent}%', + 'misc.selfUpdateReady': + 'Atualização {version} do launcher pronta para instalar', + 'misc.selfUpdateInstallNow': 'Instalar agora', + 'misc.tabCrashed': '{tab} travou', + 'misc.somethingWentWrong': 'Algo deu errado!', + 'misc.uncaughtError': '{name} não tratado: {message}', + 'misc.unknownError': 'Erro desconhecido', + 'misc.copyError': 'Copiar erro', + 'misc.reload': 'Recarregar' +}; + +const ruRU: Dict = { + 'tab.news': 'Новости', + 'tab.tweaks': 'Твики', + 'tab.addons': 'Аддоны', + 'tab.mods': 'Моды', + 'topbar.settings': 'Настройки', + 'topbar.minimize': 'Свернуть', + 'topbar.quit': 'Выход', + 'topbar.language': 'Язык', + 'quit.title': 'Выйти?', + 'quit.warn': 'Игра обновляется. Выход сейчас может вызвать проблемы.', + 'quit.return': 'Назад', + 'launch.verifying': 'Проверка', + 'launch.play': 'Играть', + 'launch.retry': 'Повторить', + 'launch.install': 'Установить', + 'launch.update': 'Обновить', + 'launch.updating': 'Обновление', + 'launch.applying': 'Применение', + 'launch.updateAvailable': 'Доступно обновление!', + 'launch.modsChanged': 'Моды изменены, примените перед игрой', + 'launch.upToDate': 'Всё обновлено!', + 'launch.version': 'Версия: {version}', + 'launch.errorLabel': 'Ошибка:', + 'launch.serverFail': 'Не удалось подключиться к серверу обновлений', + 'launch.localVersion': 'Вы можете запустить локальную версию {version}', + 'launch.tryLater': 'Повторите попытку позже', + 'launch.verifyHint': 'Проверьте данные игры, нажав Повторить.', + 'launch.remaining': 'осталось', + 'launch.calculating': 'вычисление…', + 'launch.onDisk': 'на диске', + 'tweaks.raidVisuals.label': 'Улучшенные эффекты рейдов', + 'tweaks.raidVisuals.text': + 'Дополнительная загрузка (~9 МБ). Добавляет более заметные отметки на земле и звуки для способностей рейдовых боссов, автоматически синхронизируется с сервером. (также известен как Patch-O)', + 'tweaks.alwaysAutoLoot.label': 'Всегда автосбор', + 'tweaks.alwaysAutoLoot.text': + 'Меняет поведение автосбора на противоположное: всегда автоматически собирать добычу, а ручной сбор включается зажатой клавишей.', + 'tweaks.largeAddress.label': 'Large Address Aware', + 'tweaks.largeAddress.text': 'Позволяет игре использовать более 2 ГБ памяти.', + 'tweaks.nameplateRange.label': 'Дальность индикаторов', + 'tweaks.nameplateRange.text': + 'Увеличивает расстояние, на котором видны индикаторы (nameplate). [Vanilla: 20] [Classic: 41]', + 'tweaks.cameraHeading': 'Камера', + 'tweaks.fieldOfView.label': 'Поле зрения', + 'tweaks.fieldOfView.text': + 'Рекомендуется для широкоэкранных разрешений. [Vanilla: 90] [Tweaks: 110]', + 'tweaks.farClip.label': 'Дальность прорисовки', + 'tweaks.farClip.text': + 'Увеличивает максимальную дальность прорисовки. Слишком высокие значения могут привести к вылету при входе в мир. [Vanilla: 777]', + 'tweaks.farClip.recommendedHint': 'Рекомендовано для вашего ПК: {value}.', + 'tweaks.frillDistance.label': 'Дальность травы и деталей', + 'tweaks.frillDistance.text': + 'Изменяет дальность прорисовки травы и мелких деталей земли. [Vanilla: 70] [Tweaks: 300]', + 'tweaks.cameraDistance.label': 'Дальность камеры', + 'tweaks.cameraDistance.text': + 'Увеличивает максимальное отдаление камеры. [Vanilla: 50] [Max:100]', + 'tweaks.soundsHeading': 'Звук', + 'tweaks.soundInBackground.label': 'Звук в фоне', + 'tweaks.soundInBackground.text': + 'Позволяет воспроизводить звуки игры, когда она свёрнута.', + 'tweaks.applyFailed': 'Не удалось применить твики: {message}', + 'tweaks.highlighted': 'Выделенные', + 'tweaks.recommendedNote': 'параметры рекомендуются и включены по умолчанию', + 'tweaks.reset': 'Сбросить', + 'tweaks.apply': 'Применить', + 'mods.update': 'обновление', + 'mods.ignoreUpdates': 'Игнорировать обновления', + 'mods.title': 'ПОЛЬЗОВАТЕЛЬСКИЕ МОДЫ', + 'mods.unsavedChanges': 'несохранённые изменения', + 'mods.warning': + 'Включение пользовательских модов может не дать прироста производительности или даже вызвать вылеты игры в зависимости от вашей системы. Если возникнут проблемы, попробуйте их отключить.', + 'mods.enableRequired': 'Включить {mods}, требуемые выбранными вами модами.', + 'mods.depRequired': + '{mod} должен быть включён. Он требуется для {requiredBy}.', + 'mods.highlighted': 'Выделенные', + 'mods.highlightedRecommended': 'моды рекомендуются.', + 'mods.apply': 'Применить', + 'mods.cantApplyYet': 'ПОКА НЕЛЬЗЯ ПРИМЕНИТЬ', + 'mods.close': 'Закрыть', + 'av.blockedTitle': 'ЗАБЛОКИРОВАНО АНТИВИРУСОМ', + 'av.blockedOne': 'Windows Defender заблокировал этот мод:', + 'av.blockedMany': 'Windows Defender заблокировал эти моды:', + 'av.blockedExplain': + 'Это безопасные моды от сообщества, которые Defender ошибочно помечает как угрозу. Разрешите их один раз и повторите попытку.', + 'av.whyHappening': 'Почему так происходит?', + 'av.allowThrough': 'Разрешить в антивирусе', + 'av.addedRetry': 'Добавлено. Закройте это окно и повторите попытку.', + 'av.close': 'Закрыть', + 'av.whyTitle': 'ПОЧЕМУ АНТИВИРУС ПОМЕЧАЕТ МОДЫ', + 'av.whyIntro': + 'Некоторые из модов, устанавливаемых лаунчером, помечаются Windows Defender (или другим антивирусом) как угроза, например «{detection}». Это', + 'av.falsePositive': 'ложное срабатывание', + 'av.whatSetsItOff': 'Что вызывает срабатывание', + 'av.whatSetsItOffIntro': + 'Эти моды модернизируют 20-летний клиент игры 1.12, используя два приёма, которые выглядят подозрительно для эвристик антивируса:', + 'av.dllInjection': 'Внедрение DLL', + 'av.dllInjectionText': + 'моды загружают дополнительный код в работающую игру, чтобы добавить функции (компенсацию пинга, улучшенные индикаторы, производительность). Внедрение кода в чужую программу — это то, что делают и вредоносные программы, поэтому одно лишь такое поведение вызывает срабатывание сканера.', + 'av.exePatching': 'Патчинг исполняемого файла', + 'av.exePatchingText': + 'некоторые моды изменяют WoW.exe, чтобы снять старые ограничения движка (например, позволить 32-битному клиенту использовать 4 ГБ ОЗУ вместо 2 ГБ). Изменение исполняемого файла — ещё один паттерн, характерный для вредоносных программ.', + 'av.heuristicNote': + 'Поскольку это срабатывания на основе поведения (эвристические), а не совпадения с реальными вредоносными программами, легитимные моды, использующие эти приёмы, попадают под ложные срабатывания как обобщённый «троян» или «hacktool».', + 'av.whatModsDo': 'Что моды делают на самом деле', + 'av.modVanillaFixes': + 'загрузчик, который исправляет подёргивания и ошибки таймеров и запускает остальные моды.', + 'av.modNampower': + 'компенсация пинга, чтобы заклинания ощущались отзывчиво при высокой задержке.', + 'av.modSuperWowUnitXp': + 'расширенный Lua, настоящие индикаторы, улучшения камеры и прицеливания.', + 'av.modDxvk': + 'переводит старый DirectX 9 в современный Vulkan для значительного прироста FPS.', + 'av.howToVerify': 'Как проверить эти утверждения', + 'av.howToVerifyText': + 'Это проекты сообщества с открытым исходным кодом. Вы можете прочитать каждую строку кода любого мода по ссылке на исходники рядом с ним в списке модов, а также найти название обнаружения (например, «{detection}») и убедиться, что это задокументированное ложное срабатывание на этих инструментах.', + 'av.whatAllowDoesTitle': 'Что делает «Разрешить в антивирусе»', + 'av.whatAllowDoesIntro': + 'Эта функция запрашивает у Windows права администратора (одно окно, нажмите «Да»), затем запускает встроенную в Windows', + 'av.whatAllowDoesIntroAfter': + 'команду, чтобы добавить несколько записей в список исключений Windows Defender:', + 'av.exclusionFoldersBefore': 'Вашу', + 'av.gameFolder': 'папку игры', + 'av.exclusionFoldersAnd': 'и', + 'av.launcherFolder': 'папку лаунчера', + 'av.exclusionFoldersAfter': + ', чтобы файлы модов в них больше не сканировались и не помещались в карантин.', + 'av.exclusionExesAnd': 'и', + 'av.exclusionExesAfter': + 'как разрешённые процессы, чтобы они не блокировались во время работы.', + 'av.whatAllowDoesOutro': + 'Это то же самое, что вы сделали бы вручную в разделе «Безопасность Windows» → «Защита от вирусов и угроз» → «Исключения». Она ничего не устанавливает, не запускает стороннее ПО и не меняет ничего за пределами этих конкретных путей.', + 'av.back': 'Назад', + 'addons.sectionInstalled': 'Установленные', + 'addons.sectionRecommended': 'Рекомендуемые', + 'addons.sectionAvailable': 'Доступные', + 'addons.checkForUpdates': 'Проверить обновления', + 'addons.addCustomGitAddon': 'Добавить аддон из git', + 'addons.updateAll': 'Обновить все', + 'addons.everythingUpToDate': 'Всё актуально.', + 'addons.installAddon': 'Установить аддон', + 'addons.previewAlt': 'Превью', + 'addons.readyToInstall': 'Готов к установке', + 'addons.invalidGitUrl': 'Недействительный URL git-репозитория', + 'addons.install': 'Установить', + 'addons.warnIncorrectVersionFull': + 'Похоже, этот аддон сделан для другой версии игры ({version}), и он может работать некорректно', + 'addons.warnIncorrectVersionShort': 'Неверная версия', + 'addons.warnMissingDependenciesFull': + 'У этого аддона отсутствуют зависимости: {deps}', + 'addons.warnMissingDependenciesShort': 'Отсутствуют зависимости', + 'addons.details': 'Подробнее', + 'addons.openOnGithub': 'Открыть {url} на GitHub', + 'addons.upToDate': 'Актуально', + 'addons.notVersioned': 'Без версии', + 'addons.update': 'Обновить', + 'addons.download': 'Скачать', + 'addons.deleteConfirmTitle': 'Вы уверены?', + 'addons.deleteConfirmBody': 'Вы уверены, что хотите удалить аддон {folder}?', + 'addons.deleteConfirmFiles': 'Это удалит все файлы в папке аддона.', + 'addons.delete': 'Удалить', + 'addons.remove': 'Убрать', + 'addons.detailSource': 'Исходник', + 'addons.openOnGithubShort': 'Открыть на GitHub', + 'addons.detailContributions': 'Вклад', + 'addons.detailAddonVersion': 'Версия аддона', + 'addons.detailDependencies': 'Зависимости', + 'addons.optional': '(необязательно)', + 'prefs.mirrorOffline': 'недоступно', + 'prefs.mirrorChecking': 'проверка…', + 'prefs.mirrorOnline': 'в сети', + 'prefs.title': 'НАСТРОЙКИ', + 'prefs.installLocation': 'ПАПКА УСТАНОВКИ:', + 'prefs.openFolder': 'Открыть папку', + 'prefs.notSelected': 'Не выбрано', + 'prefs.change': 'Изменить', + 'prefs.downloadMirror': 'ЗЕРКАЛО ЗАГРУЗКИ:', + 'prefs.recheck': 'Перепроверить', + 'prefs.troubleshooting': 'УСТРАНЕНИЕ НЕПОЛАДОК:', + 'prefs.verifyGameFiles': 'Проверить файлы игры', + 'prefs.openLogFile': 'Открыть файл журнала', + 'prefs.allowThroughAntivirus': 'Разрешить в антивирусе', + 'prefs.exclusionAdded': + 'Добавлено. Перепроверьте файлы игры, если какие-то из них были удалены.', + 'prefs.generalSettings': 'ОБЩИЕ НАСТРОЙКИ:', + 'prefs.cleanWdb': 'Очищать WDB при каждом запуске', + 'prefs.minimizeToTray': 'Сворачивать в трей во время игры', + 'prefs.save': 'Сохранить', + 'prefs.installLocationTitle': 'Папка установки', + 'prefs.portableInfo': + 'Вы используете портативную версию лаунчера. Папка установки определяется расположением исполняемого файла лаунчера.', + 'prefs.errorLabel': 'Ошибка: ', + 'prefs.wowExeNotFound': + '{exe} не найден в текущей папке. Закройте лаунчер и переместите его в папку с клиентом WoW 1.12.', + 'prefs.selectDirectory': 'Выберите папку для установки клиента игры.', + 'prefs.upgradeExisting': + 'Вы также можете выбрать папку с уже установленным Turtle WoW или Vanilla WoW, и она будет автоматически обновлена.', + 'prefs.installDirectory': 'Папка установки:', + 'prefs.noClientHere': + 'В этой папке нет {exe}. Лаунчер скачает сюда новый клиент, в котором не будет ваших текущих аддонов и настроек. Если игра уже установлена, выберите её папку.', + 'prefs.noClientHereConfirm': 'Понятно — скачать новый клиент в эту папку', + 'prefs.confirm': 'Подтвердить', + 'misc.newsTitle': 'Новости', + 'misc.newsByAuthor': 'от {author}', + 'misc.newsReadMore': 'Читать далее', + 'misc.refresh': 'Обновить', + 'misc.newsLoading': 'Загрузка новостей...', + 'misc.newsError': 'Не удалось получить ленту новостей.', + 'misc.newsEmpty': 'Новостей пока нет — загляните позже.', + 'misc.tryAgain': 'Повторить', + 'misc.comingSoon': 'Скоро...', + 'misc.selfUpdateCheckFailed': 'Не удалось проверить обновления: {message}', + 'misc.selfUpdateAvailable': + 'Доступно обновление лаунчера {version} — подготовка загрузки…', + 'misc.selfUpdateDownloading': 'Загрузка обновления {version} · {percent}%', + 'misc.selfUpdateReady': 'Обновление лаунчера {version} готово к установке', + 'misc.selfUpdateInstallNow': 'Установить сейчас', + 'misc.tabCrashed': 'Вкладка {tab} аварийно завершилась', + 'misc.somethingWentWrong': 'Что-то пошло не так!', + 'misc.uncaughtError': 'Необработанная ошибка {name}: {message}', + 'misc.unknownError': 'Неизвестная ошибка', + 'misc.copyError': 'Скопировать ошибку', + 'misc.reload': 'Перезагрузить' +}; + +export const translations: Record = { + enUS, + deDE, + zhCN, + esES, + ptBR, + ruRU +}; diff --git a/src/renderer/index.css b/src/renderer/index.css new file mode 100644 index 0000000..3d4bacd --- /dev/null +++ b/src/renderer/index.css @@ -0,0 +1,497 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@font-face { + font-family: 'fontin'; + src: url('./assets/FontinSans-Regular.otf'); +} + +@font-face { + font-family: 'din'; + src: url('./assets/DINPro-Regular.otf'); +} + +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + height: 100vh; +} + +body { + -webkit-user-select: text; + user-select: text; +} + +::selection { + background: rgb(248 156 66 / 0.45); + color: #fff; +} + +#root { + position: relative; + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + overflow-x: auto; +} + +*:focus { + outline: none; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + appearance: none; + margin: 0; +} + +input[type='number'] { + -moz-appearance: textfield; + appearance: textfield; +} + +::-webkit-scrollbar { + @apply w-2; + @apply h-2; + + &-track { + background: transparent; + } + + &-thumb { + display: none; + @apply bg-blueGray/40; + + :hover& { + display: initial; + cursor: pointer; + } + + &:hover { + @apply bg-blueGray; + } + } + + &-corner { + display: none; + } +} + +.gutter { + background: transparent; + @apply transition-colors; + + &:hover { + @apply bg-orange/40; + } + + &&-horizontal { + @apply -mx-1; + cursor: col-resize; + } + + &&-vertical { + @apply -my-1; + cursor: row-resize; + } +} + +@layer components { + :not(svg, svg *) { + color: white; + @apply font-din; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 26px; + cursor: default; + } + + h1, + .h1 { + @apply font-fontin; + font-style: normal; + font-weight: 700; + font-size: 78px; + line-height: 76px; + letter-spacing: 0.03em; + text-transform: uppercase; + } + + h2, + .h2 { + @apply font-fontin; + font-weight: 700; + font-size: 54px; + line-height: 58px; + letter-spacing: 0.03em; + text-transform: uppercase; + } + + h3, + .h3 { + @apply font-fontin; + font-weight: 400; + font-size: 32px; + line-height: 38px; + letter-spacing: 0.03em; + text-transform: uppercase; + } + + h4, + .h4 { + @apply font-fontin; + font-weight: 400; + font-size: 20px; + line-height: 26px; + letter-spacing: 0.03em; + text-transform: uppercase; + } + + .l1 { + font-size: 18px; + line-height: 32px; + } + + .l2 { + font-size: 24px; + line-height: 36px; + } + + .s1 { + font-size: 14px; + line-height: 20px; + } + + .tw-color { + display: inline; + @apply bg-gradient-to-t from-yellow to-pink; + -webkit-box-decoration-break: clone; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + .tw-surface { + position: relative; + @apply border border-blueGray/20 bg-darkGray/70 p-4; + box-shadow: rgb(0 0 0 / 45%) 0px 25px 20px -20px; + + & hr { + @apply -mx-4 text-blueGray/20; + } + } + + .tw-dialog { + position: relative; + @apply border border-blueGray/20 bg-darkGray/70 p-3; + box-shadow: rgb(0 0 0 / 45%) 0px 25px 20px -20px; + @apply relative flex w-3/5 flex-col gap-2; + + & hr { + @apply -mx-3 text-blueGray/20; + } + } + + .parchment-post { + position: relative; + color: #2b1a0c; + background-color: #e9dab5; + background-size: cover; + background-position: center; + @apply border p-4; + border-color: rgba(60, 40, 15, 0.4); + box-shadow: rgb(0 0 0 / 45%) 0px 25px 20px -20px; + + & .parchment-post-title { + @apply font-fontin uppercase; + font-size: 18px; + line-height: 24px; + letter-spacing: 0.03em; + color: #3a230f; + } + + & .parchment-post-meta { + font-size: 14px; + line-height: 20px; + font-style: italic; + color: #6b5836; + } + + & .parchment-post-muted { + color: #7a6a47; + } + + & hr { + @apply -mx-4; + border: 0; + border-top: 1px solid rgba(60, 40, 15, 0.25); + } + + & button { + color: #6b3e15; + } + & button:hover, + & button:focus { + color: #9c4a12; + } + + & .parchment-post-body, + & .parchment-post-body * { + color: #2b1a0c; + } + & .parchment-post-body { + font-size: 15px; + line-height: 24px; + word-break: break-word; + overflow-wrap: anywhere; + } + & .parchment-post-body a { + color: #6b3e15; + text-decoration: underline; + cursor: pointer; + } + & .parchment-post-body a:hover { + color: #9c4a12; + } + & .parchment-post-body p { + margin: 6px 0; + } + & .parchment-post-body strong, + & .parchment-post-body b { + font-weight: 700; + } + & .parchment-post-body em, + & .parchment-post-body i { + font-style: italic; + } + & .parchment-post-body ul { + list-style: disc; + padding-left: 20px; + margin: 6px 0; + } + & .parchment-post-body ol { + list-style: decimal; + padding-left: 20px; + margin: 6px 0; + } + & .parchment-post-body blockquote { + border-left: 3px solid #b08a57; + background: rgba(120, 80, 30, 0.08); + padding: 8px 12px; + margin: 8px 0; + font-style: italic; + } + & .parchment-post-body code, + & .parchment-post-body pre, + & .parchment-post-body .codebox { + font-family: monospace; + background: rgba(60, 40, 15, 0.1); + border: 1px solid rgba(60, 40, 15, 0.2); + border-radius: 2px; + padding: 8px; + display: block; + white-space: pre-wrap; + word-break: break-word; + } + & .parchment-post-body img { + max-width: 100%; + height: auto; + } + & .parchment-post-body h1, + & .parchment-post-body h2, + & .parchment-post-body h3, + & .parchment-post-body h4 { + color: #3a230f; + } + } + + .tw-hocus { + @apply hocus:text-orange hocus:drop-shadow-[0px_0px_15px_white]; + } + + .tw-input { + @apply rounded-[1px]; + @apply border border-gray/40 bg-darkerGray; + @apply p-2; + @apply placeholder:text-gray; + @apply focus:border-blueGray; + + &-underline { + @apply tw-input; + @apply border-0; + @apply border-b; + @apply bg-[transparent]; + } + } + + .tw-button { + overflow: hidden; + position: relative; + cursor: pointer; + flex-shrink: 0; + @apply py-2; + @apply px-4; + @apply bg-darkGray; + @apply border; + @apply rounded-[1px]; + + &:not(&-primary) { + background: linear-gradient(#f1c22d40, #ff775740); + @apply border-darkBrown; + + & > span { + background: linear-gradient(#f1c22d, #ff7757); + -webkit-background-clip: text; + } + + & svg { + stroke: #fb9f3a; + } + + &::before { + @apply bg-orange; + } + } + + &&-primary { + @apply bg-darkGreen/30; + @apply border-[#C8FF0022]; + + & > span { + background: linear-gradient(#f7ff8a, #8dd958); + -webkit-background-clip: text; + } + + & svg { + stroke: #ccf068; + } + + &::before { + @apply bg-warmGreen; + } + + &:hover, + &:focus { + &::after { + @apply bg-warmGreen; + } + } + } + + & > span { + @apply flex items-center justify-center; + @apply gap-2; + @apply font-fontin; + @apply font-bold; + @apply uppercase; + font-size: 20px; + line-height: 30px; + letter-spacing: 2px; + -webkit-box-decoration-break: clone; + -webkit-text-fill-color: transparent; + } + + & svg { + font-size: 10px; + line-height: 30px; + } + + &:hover, + &:focus { + & > span { + background: white; + -webkit-background-clip: text; + } + + & svg { + stroke: white; + } + + &::before { + top: 9px; + bottom: 22px; + left: 22px; + right: 22px; + } + + &::after { + content: ''; + position: absolute; + top: 12px; + bottom: -46px; + left: 12px; + right: 12px; + border-radius: 50%; + @apply bg-orange; + opacity: 0.75; + mix-blend-mode: hard-light; + filter: blur(25px); + } + } + + &::before { + content: ''; + position: absolute; + top: 5px; + bottom: 5px; + left: 18px; + right: 18px; + border-radius: 50%; + opacity: 0.75; + mix-blend-mode: hard-light; + filter: blur(25px); + } + } + + .tw-loading { + @apply absolute inset-0 transition-all; + background: linear-gradient( + 90deg, + rgba(255, 119, 87, 0) 0%, + #f89c42 30%, + #f1c22d 50%, + #f89c42 70%, + rgba(255, 119, 87, 0) 100% + ); + transition-duration: 300ms; + + &-wrapper { + @apply relative w-full before:absolute; + height: 6px; + &::before { + @apply inset-0 opacity-20; + background: linear-gradient( + 90deg, + rgba(146, 147, 145, 0) 0%, + #929391 13%, + #929391 87%, + rgba(146, 147, 145, 0) 100% + ); + } + } + + &-unknown { + @apply animate-progress opacity-20; + background-image: linear-gradient( + -45deg, + #929391, + #929391 33%, + transparent 33%, + transparent 66%, + #929391 66%, + #929391 + ); + background-size: 1rem 100%; + } + } +} diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..4c6afe2 --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,12 @@ + + + + + Octo Launcher + + + +
+ + + diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx new file mode 100644 index 0000000..e627ff9 --- /dev/null +++ b/src/renderer/main.tsx @@ -0,0 +1,68 @@ +import { StrictMode } from 'react'; +import ReactDOM from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { loggerLink } from '@trpc/client'; +import { ipcLink } from 'electron-trpc/renderer'; +import superjson from 'superjson'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { getQueryKey } from '@trpc/react-query'; +import log from 'electron-log/renderer'; + +import { api } from './utils/api'; +import App from './App'; +import ErrorBoundary from './ErrorBoundary'; +import { LocaleProvider } from './i18n'; + +import './index.css'; + +window.addEventListener('error', e => { + log.error('Uncaught error:', e.error ?? e.message, e.filename, e.lineno); +}); +window.addEventListener('unhandledrejection', e => { + log.error('Unhandled promise rejection:', e.reason); +}); + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + staleTime: Infinity + } + } +}); + +queryClient.setMutationDefaults(getQueryKey(api.preferences.set), { + onSuccess: v => + queryClient.setQueryData( + getQueryKey(api.preferences.get, undefined, 'query'), + v + ) +}); + +const trpcClient = api.createClient({ + transformer: superjson, + links: [ + loggerLink({ + enabled: opts => + process.env.NODE_ENV === 'development' || + (opts.direction === 'down' && opts.result instanceof Error) + }), + ipcLink() + ] +}); + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + {import.meta.env.DEV && } + + + + +); diff --git a/src/renderer/utils/api.ts b/src/renderer/utils/api.ts new file mode 100644 index 0000000..83b2875 --- /dev/null +++ b/src/renderer/utils/api.ts @@ -0,0 +1,9 @@ +import { createTRPCReact } from '@trpc/react-query'; +import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; + +import { type AppRouter } from '~main/types'; + +export const api = createTRPCReact(); + +export type RouterInputs = inferRouterInputs; +export type RouterOutputs = inferRouterOutputs; diff --git a/src/renderer/utils/usePreventDefaultEvents.ts b/src/renderer/utils/usePreventDefaultEvents.ts new file mode 100644 index 0000000..cab6888 --- /dev/null +++ b/src/renderer/utils/usePreventDefaultEvents.ts @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; + +const allowedElements = ['INPUT', 'TEXTAREA']; + +const isClipboardShortcut = (e: KeyboardEvent) => + (e.ctrlKey || e.metaKey) && + ['a', 'c', 'v', 'x'].includes(e.key.toLowerCase()); + +const usePreventDefaultEvents = () => { + useEffect(() => { + const disableKeyboardEvents = (e: KeyboardEvent) => { + if (allowedElements.includes((e.target as HTMLElement).tagName)) return; + if (isClipboardShortcut(e)) return; + e.preventDefault(); + }; + + window.addEventListener('keydown', disableKeyboardEvents, true); + window.addEventListener('keyup', disableKeyboardEvents, true); + + return () => { + window.removeEventListener('keydown', disableKeyboardEvents, true); + window.removeEventListener('keyup', disableKeyboardEvents, true); + }; + }, []); +}; + +export default usePreventDefaultEvents; diff --git a/src/renderer/utils/useScrollHint.ts b/src/renderer/utils/useScrollHint.ts new file mode 100644 index 0000000..08a824d --- /dev/null +++ b/src/renderer/utils/useScrollHint.ts @@ -0,0 +1,61 @@ +import { useLayoutEffect, useRef } from 'react'; + +const FADE_PX = 24; + +const setScrollHint = (tar: HTMLElement) => { + const top = tar.scrollTop > 0 && tar.scrollHeight !== tar.clientHeight; + const bottom = tar.scrollTop < tar.scrollHeight - tar.offsetHeight; + + const topStr = top ? 'true' : 'false'; + const bottomStr = bottom ? 'true' : 'false'; + if (topStr === tar.dataset.top && bottomStr === tar.dataset.bottom) return; + + tar.dataset.top = topStr; + tar.dataset.bottom = bottomStr; + + if (!top && !bottom) { + tar.style.webkitMaskImage = ''; + return; + } + + tar.style.webkitMaskImage = `linear-gradient(${ + top ? `transparent, black calc(${FADE_PX}px)` : '' + }${top && bottom ? ', ' : ''}${ + bottom ? `black calc(100% - ${FADE_PX}px), transparent` : '' + })`; +}; + +const useScrollHint = () => { + const ref = useRef(null); + useLayoutEffect(() => { + const current = ref.current; + if (!current) return; + + let scheduled = false; + const schedule = () => { + if (scheduled) return; + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; + setScrollHint(current); + }); + }; + + schedule(); + + const observer = new ResizeObserver(schedule); + observer.observe(current); + current.addEventListener('scroll', schedule, { passive: true }); + window.addEventListener('resize', schedule); + + return () => { + observer.disconnect(); + current?.removeEventListener('scroll', schedule); + window.removeEventListener('resize', schedule); + }; + }, []); + + return ref; +}; + +export default useScrollHint; diff --git a/src/renderer/utils/zodResolver.ts b/src/renderer/utils/zodResolver.ts new file mode 100644 index 0000000..582568c --- /dev/null +++ b/src/renderer/utils/zodResolver.ts @@ -0,0 +1,9 @@ +import { type Resolver, type FieldValues } from 'react-hook-form'; +import type { z } from 'zod'; +import { zodResolver as resolver } from '@hookform/resolvers/zod'; + +const zodResolver = ( + schema: z.ZodType +): Resolver>> => resolver(schema); + +export default zodResolver; diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..dd0f022 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,76 @@ +import { type Config } from 'tailwindcss'; +import plugin from 'tailwindcss/plugin'; + +export default { + content: ['./src/renderer/index.html', './src/renderer/**/*.{js,ts,jsx,tsx}'], + theme: { + spacing: { + 0: '0', + 1: '4px', + 2: '8px', + 3: '16px', + 4: '32px', + 5: '64px', + 6: '128px', + 7: '256px', + 8: '512px' + }, + screens: { + 'sm': '640px', + 'md': '768px', + 'lg': '1024px', + 'xl': '1280px', + '2xl': '1536px' + }, + colors: { + inherit: 'inherit', + current: 'currentColor', + white: '#FFF', + yellow: '#F1C22D', + pink: '#FF7757', + orange: '#FB8A4C', + brown: '#764502', + red: '#EE3333', + darkBrown: '#291105', + darkGray: '#0F0D0C', + darkerGray: '#181412', + darkPurple: '#271F30', + purple: '#403561', + gray: '#5E5B55', + lightGray: '#9E9783', + blueGray: '#929391', + warmGreen: '#C5CB63', + green: '#8DD958', + darkGreen: '#358D3E' + }, + extend: { + fontFamily: { + fontin: ['fontin'], + din: ['din'] + }, + animation: { + progress: 'progress 2s linear infinite' + }, + keyframes: { + progress: { + '0%': { backgroundPosition: '1rem 0' }, + '100%': { backgroundPosition: '0 0' } + } + } + } + }, + plugins: [ + require('@tailwindcss/container-queries'), + plugin(({ addVariant, addComponents }) => { + addVariant('hocus', ['&:hover', '&:focus']); + addVariant('hocus-within', ['&:hover', '&:focus-within']); + addVariant('hover-row', ['&:hover>div:first-child']); + addComponents({ + '.tw-surface': {}, + '.tw-color': {}, + '.h1': {}, + '.text-size-inherit': { fontSize: 'inherit', lineHeight: 'inherit' } + }); + }) + ] +} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..48c95d4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.web.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~common/*": ["./src/common/*"], + "~main/*": ["./src/main/*"], + "~renderer/*": ["./src/renderer/*"], + "~build/*": ["./build/*"] + } + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..58db518 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", + "include": [ + "src/renderer/env.d.ts", + "electron.vite.config.*", + "src/common/**/*.ts", + "src/main/**/*.ts", + "src/preload/**/*.ts", + "tailwind.config.ts", + "postcss.config.cjs" + ], + "compilerOptions": { + "composite": true, + "types": ["electron-vite/node"], + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "~common/*": ["./src/common/*"], + "~main/*": ["./src/main/*"], + "~renderer/*": ["./src/renderer/*"], + "~build/*": ["./build/*"] + } + } +} diff --git a/tsconfig.web.json b/tsconfig.web.json new file mode 100644 index 0000000..156ccff --- /dev/null +++ b/tsconfig.web.json @@ -0,0 +1,22 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", + "include": [ + "src/renderer/env.d.ts", + "src/common/**/*.ts", + "src/renderer/**/*", + "src/main/types.d.ts", + "src/preload/window.d.ts" + ], + "compilerOptions": { + "composite": true, + "jsx": "react-jsx", + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "~common/*": ["./src/common/*"], + "~main/*": ["./src/main/*"], + "~renderer/*": ["./src/renderer/*"], + "~build/*": ["./build/*"] + } + } +}