From 87ddf1499dc9937c99e3ff76b9cba56f75e15676 Mon Sep 17 00:00:00 2001 From: bigjakk Date: Sun, 1 Mar 2026 06:38:15 -0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Krunker=20Civili?= =?UTF-8?q?an=20Client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-platform Krunker.io game client forked from Krunker Police Client with all KPD/moderator features stripped: no KPD auth, OBS recording, evidence uploads, yt-dlp, bytenode, or code obfuscation. Retained: unlimited FPS (custom Electron 42), ad blocking, resource swapper, matchmaker, userscripts, chat translator, Discord RPC, alt account manager, configurable keybinds, and advanced Chromium flags. Co-Authored-By: Claude Opus 4.6 --- .gitea/workflows/build-release.yml | 138 + .gitea/workflows/mirror-releases.yml | 82 + .gitignore | 17 + README.md | 292 ++ build/generate-icons.sh | 17 + build/icon.ico | Bin 0 -> 47410 bytes build/icon.png | Bin 0 -> 28227 bytes build/icon.svg | 4 + build/krunker-civilian-client.desktop | 10 + electron-build/BUILD.md | 193 + electron-build/args.release.gn | 8 + electron-build/input-priority-fix.patch | 29 + electron-builder.yml | 72 + package-lock.json | 6272 +++++++++++++++++++++++ package.json | 34 + scripts/download-electron.js | 205 + src/main/client-ui.ts | 587 +++ src/main/config.ts | 178 + src/main/discord-rpc.ts | 285 + src/main/index.ts | 670 +++ src/main/logger.ts | 80 + src/main/platform.ts | 128 + src/main/swapper.ts | 95 + src/main/update-window.ts | 96 + src/main/updater.ts | 212 + src/main/userscripts.ts | 99 + src/preload/index.ts | 1652 ++++++ src/preload/matchmaker.ts | 190 + src/preload/translator.ts | 360 ++ src/preload/userscripts.ts | 258 + src/preload/utils.ts | 75 + tsconfig.json | 20 + vite.main.config.ts | 31 + vite.preload.config.ts | 22 + 34 files changed, 12411 insertions(+) create mode 100644 .gitea/workflows/build-release.yml create mode 100644 .gitea/workflows/mirror-releases.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build/generate-icons.sh create mode 100644 build/icon.ico create mode 100644 build/icon.png create mode 100644 build/icon.svg create mode 100644 build/krunker-civilian-client.desktop create mode 100644 electron-build/BUILD.md create mode 100644 electron-build/args.release.gn create mode 100644 electron-build/input-priority-fix.patch create mode 100644 electron-builder.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/download-electron.js create mode 100644 src/main/client-ui.ts create mode 100644 src/main/config.ts create mode 100644 src/main/discord-rpc.ts create mode 100644 src/main/index.ts create mode 100644 src/main/logger.ts create mode 100644 src/main/platform.ts create mode 100644 src/main/swapper.ts create mode 100644 src/main/update-window.ts create mode 100644 src/main/updater.ts create mode 100644 src/main/userscripts.ts create mode 100644 src/preload/index.ts create mode 100644 src/preload/matchmaker.ts create mode 100644 src/preload/translator.ts create mode 100644 src/preload/userscripts.ts create mode 100644 src/preload/utils.ts create mode 100644 tsconfig.json create mode 100644 vite.main.config.ts create mode 100644 vite.preload.config.ts diff --git a/.gitea/workflows/build-release.yml b/.gitea/workflows/build-release.yml new file mode 100644 index 0000000..4762072 --- /dev/null +++ b/.gitea/workflows/build-release.yml @@ -0,0 +1,138 @@ +name: Build and Release + +on: + push: + branches: + - main + +jobs: + build-and-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check if version already released + id: version-check + env: + GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }} + run: | + VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/') + TAG="v$VERSION" + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "TAG=$TAG" >> "$GITHUB_OUTPUT" + + STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + "https://gitea.crjlab.net/api/v1/repos/bigjakk/krunker-civilian-client/releases/tags/$TAG" \ + -H "Authorization: token $GITEA_TOKEN") + + if [ "$STATUS" = "200" ]; then + echo "Release $TAG already exists, skipping build" + echo "SKIP=true" >> "$GITHUB_OUTPUT" + else + echo "No release for $TAG, proceeding with build" + echo "SKIP=false" >> "$GITHUB_OUTPUT" + fi + + - name: Setup Node.js + if: steps.version-check.outputs.SKIP == 'false' + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install system dependencies + if: steps.version-check.outputs.SKIP == 'false' + run: | + dpkg --add-architecture i386 + apt-get update -qq + apt-get install -y --no-install-recommends \ + wine wine32 wine64 \ + libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2t64 \ + libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \ + libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \ + libcairo2 libasound2t64 libgtk-3-0 + WINEDEBUG=-all wine wineboot --init || true + + - name: Install dependencies + if: steps.version-check.outputs.SKIP == 'false' + run: npm ci + + - name: Build source + if: steps.version-check.outputs.SKIP == 'false' + run: npm run build + + - name: Build Windows distributables + if: steps.version-check.outputs.SKIP == 'false' + env: + WINEDEBUG: "-all" + run: npx electron-builder --win -c.electronDist=node_modules/electron/dist-win --publish never + + - name: Build Linux distributables + if: steps.version-check.outputs.SKIP == 'false' + run: npx electron-builder --linux --publish never + + - name: Report build sizes + if: steps.version-check.outputs.SKIP == 'false' + run: | + echo "=== Build output sizes ===" + ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true + echo "=== Electron dist-win (patched Windows) ===" + du -sh node_modules/electron/dist-win/ 2>/dev/null || true + echo "=== Electron dist (stock Linux) ===" + du -sh node_modules/electron/dist/ 2>/dev/null || true + echo "=== Unpacked Windows build ===" + du -sh out/win-unpacked/ 2>/dev/null || true + du -sh out/win-unpacked/resources/ 2>/dev/null || true + du -sh out/win-unpacked/locales/ 2>/dev/null || true + + - name: Create release and upload assets + if: steps.version-check.outputs.SKIP == 'false' + env: + GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }} + TAG: ${{ steps.version-check.outputs.TAG }} + run: | + GITEA_BASE="https://gitea.crjlab.net" + REPO="bigjakk/krunker-civilian-client" + + # Create tag + curl -s -X POST "$GITEA_BASE/api/v1/repos/$REPO/tags" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\": \"$TAG\", \"message\": \"$TAG\", \"target\": \"$GITHUB_SHA\"}" + + # Create release + RESPONSE=$(curl -s -X POST "$GITEA_BASE/api/v1/repos/$REPO/releases" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"$TAG\", + \"name\": \"$TAG\", + \"body\": \"Automated build for $TAG\", + \"draft\": false, + \"prerelease\": false + }") + + RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id') + echo "Created release ID: $RELEASE_ID" + + if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then + echo "Failed to create release:" + echo "$RESPONSE" + exit 1 + fi + + # Upload all built artifacts + for file in out/*.exe out/*.AppImage out/*.deb; do + [ -f "$file" ] || continue + FILENAME=$(basename "$file") + SAFE_NAME=$(echo "$FILENAME" | tr ' ' '_') + echo "Uploading: $SAFE_NAME ($(du -h "$file" | cut -f1))" + + curl -s -X POST \ + "$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$SAFE_NAME" \ + -H "Authorization: token $GITEA_TOKEN" \ + -F "attachment=@$file" \ + | jq -r '" -> \(.name) (\(.size) bytes)"' + done + + echo "All assets uploaded" diff --git a/.gitea/workflows/mirror-releases.yml b/.gitea/workflows/mirror-releases.yml new file mode 100644 index 0000000..c56504d --- /dev/null +++ b/.gitea/workflows/mirror-releases.yml @@ -0,0 +1,82 @@ +name: Mirror Release to KCC + +on: + release: + types: [published] + +jobs: + mirror-release: + runs-on: ubuntu-latest + steps: + - name: Mirror release and assets + env: + BASE: https://gitea.crjlab.net/api/v1 + SOURCE_REPO: bigjakk/krunker-civilian-client + DEST_REPO: bigjakk/KPC + TOKEN: ${{ secrets.DEST_GITEA_TOKEN }} + run: | + TAG="${{ github.event.release.tag_name }}" + NAME="${{ github.event.release.name }}" + BODY=$(echo '${{ toJson(github.event.release.body) }}') + + # Create tag on KPC pointing to latest main commit + SHA=$(curl -s "$BASE/repos/$DEST_REPO/branches/main" \ + -H "Authorization: token $TOKEN" | jq -r '.commit.id') + echo "Creating tag $TAG on KPC at $SHA" + + curl -s -X POST "$BASE/repos/$DEST_REPO/tags" \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\": \"$TAG\", \"message\": \"$TAG\", \"target\": \"$SHA\"}" + + # Create release on KPC + RESPONSE=$(curl -s -X POST "$BASE/repos/$DEST_REPO/releases" \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"$TAG\", + \"name\": \"$NAME\", + \"body\": $BODY, + \"draft\": ${{ github.event.release.draft }}, + \"prerelease\": ${{ github.event.release.prerelease }} + }") + + RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id') + echo "Created KPC release ID: $RELEASE_ID" + + if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then + echo "Failed to create release" + exit 1 + fi + + # Poll source release until assets appear (30s intervals, up to ~6 min) + echo "Waiting for source assets..." + for ATTEMPT in $(seq 1 12); do + RELEASE_INFO=$(curl -s "$BASE/repos/$SOURCE_REPO/releases/tags/$TAG" \ + -H "Authorization: token $TOKEN") + ASSET_COUNT=$(echo "$RELEASE_INFO" | jq '.assets | length') + echo "Attempt $ATTEMPT/12: $ASSET_COUNT asset(s)" + + [ "$ASSET_COUNT" -gt 0 ] && break + [ "$ATTEMPT" -lt 12 ] && sleep 30 + done + + if [ "$ASSET_COUNT" -eq 0 ]; then + echo "No assets found after 12 attempts. Aborting." + exit 1 + fi + + # Download and re-upload each asset to KPC + echo "$RELEASE_INFO" | jq -c '.assets[]' | while read -r asset; do + ASSET_NAME=$(echo "$asset" | jq -r '.name') + ASSET_URL=$(echo "$asset" | jq -r '.browser_download_url') + + echo "Mirroring: $ASSET_NAME" + curl -sL "$ASSET_URL" -o "/tmp/$ASSET_NAME" -H "Authorization: token $TOKEN" + + curl -s -X POST "$BASE/repos/$DEST_REPO/releases/$RELEASE_ID/assets?name=$ASSET_NAME" \ + -H "Authorization: token $TOKEN" \ + -F "attachment=@/tmp/$ASSET_NAME" | jq -r '" -> \(.name) (\(.size) bytes)"' + + rm "/tmp/$ASSET_NAME" + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2305b7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +node_modules/ +dist/ +out/ +*.log +.env +.vscode/ +.idea/ +.claude/ +*.swp +*.swo +nul +CLAUDE.md +Screenshot* +Trace-* +*.cpuprofile +*.heapprofile +userscripts/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..22ad129 --- /dev/null +++ b/README.md @@ -0,0 +1,292 @@ +# Krunker Civilian Client + +Cross-platform Electron-based game client for [Krunker.io](https://krunker.io). Loads the game directly in a BrowserWindow with GPU optimizations, unlimited FPS, persistent sessions, ad blocking, resource swapping, custom matchmaker, userscript engine, and Discord Rich Presence. + +## Quick Start + +```bash +npm install +npm start # Build all targets + launch Electron +``` + +For development with sourcemaps: + +```bash +npm run dev # Builds in dev mode + launches Electron +``` + +## Scripts + +| Script | Description | +|--------|-------------| +| `npm run dev` | Development build + launch | +| `npm start` | Full build (main + preload) + launch | +| `npm run build` | Build both Vite targets | +| `npm run build:main` | Build main process only | +| `npm run build:preload` | Build preload script only | +| `npm run dist:win` | Build + package for Windows (NSIS installer + portable) | +| `npm run dist:linux` | Build + package for Linux (AppImage + deb) | +| `npm run dist:all` | Build + package for all platforms | +| `npm run clean` | Remove `dist/` and `out/` directories | + +## Architecture + +### Custom Patched Electron 42 + +This client uses a custom-patched Electron 42 (Chromium 134, Node 24). The `--disable-frame-rate-limit` Chromium flag — required for unlimited FPS — causes input starvation ("aim freeze") on Chromium 84+. At uncapped frame rates (300+ FPS), the compositor floods the main thread task queue and input events get delayed 50-300ms, then snap to catch up. The `ImplLatencyRecovery`/`MainLatencyRecovery` features mitigated this on Chromium 87–93 but were removed in Chromium 94. + +#### The Patch + +A single-line change in Chromium's main thread scheduler (`main_thread_scheduler_impl.cc`) demotes input tasks from `kHighestPriority` to `kNormalPriority`, allowing the scheduler's built-in anti-starvation logic to fairly interleave input and compositor work: + +```diff + case MainThreadTaskQueue::QueueTraits::PrioritisationType::kInput: +- return TaskPriority::kHighestPriority; ++ return TaskPriority::kNormalPriority; +``` + +Benchmarked via CDP `Input.dispatchMouseEvent`: + +| Metric | Before | After | +|--------|--------|-------| +| p99 latency | 97ms | 34ms | +| Max latency | 308ms | 38ms | +| Events >50ms | 8.6% | 0% | +| Frames rendered | baseline | +21% | +| Mouse events processed | baseline | +9% | + +The full patch file, GN build args, and step-by-step build instructions are in [`electron-build/`](electron-build/BUILD.md). + +#### Binary Distribution + +The patched binary is downloaded automatically during `npm install` via `scripts/download-electron.js` from a Gitea release asset (`electron-patched` tag). The `electron` npm dependency (`npm:electron-nightly@42.0.0-nightly.20260227`) provides TypeScript types and the CLI entry point, but the actual binary comes from the patched download. + +FPS uncap flags (always applied when enabled): + +``` +disable-frame-rate-limit + disable-gpu-vsync + max-gum-fps=9999 +``` + +#### Building from Source + +Prerequisites: Windows 10/11 x64, ~100 GB disk, 16+ GB RAM, Visual Studio 2022 with C++ workload. + +```bash +# 1. Install depot_tools and add to PATH +git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git + +# 2. Check out Electron source (~40-60 GB) +mkdir C:\electron && cd C:\electron +gclient config --name "src/electron" --unmanaged https://github.com/nicedayzhu/electron.git@v42.0.0-nightly.20260227 +gclient sync --with_branch_heads --with_tags + +# 3. Apply the patch +cd src +git apply path/to/electron-build/input-priority-fix.patch + +# 4. Configure and build (~2-4 hours for release) +gn gen out/Release +cp path/to/electron-build/args.release.gn out/Release/args.gn +gn gen out/Release +ninja -C out/Release electron + +# 5. Create distributable zip +ninja -C out/Release electron:dist_zip +``` + +See [`electron-build/BUILD.md`](electron-build/BUILD.md) for detailed instructions, testing builds, and verification steps. + +### How the Game Loads + +The game is loaded **directly** in the main `BrowserWindow` via `win.loadURL('https://krunker.io')`. This gives the game full GPU compositing, identical to Chrome. + +### Process Model + +``` +Main Process (src/main/index.ts) + | + |-- Creates BrowserWindow loading krunker.io directly + |-- Manages session (persist:krunker), UA cleaning, permissions + |-- Ad blocking + resource swapping via webRequest.onBeforeRequest + |-- Configurable keybinds via before-input-event + |-- Matchmaker IPC trigger (sends config to preload on hotkey) + |-- CSS injection (ad hiding, client settings, matchmaker popup, keybind dialog) + |-- File-based logging (electron.log in userData/logs/) + |-- IPC handlers for window controls, config, devtools, userscripts + |-- Userscript manager (filesystem scanning, tracker, preferences) + |-- Auto-updater (Gitea releases API check, Setup.exe download + launch) + |-- Discord Rich Presence via raw IPC socket + |-- Consent dismiss via polling (no MutationObserver on main frame) + | + +-- Preload Script (src/preload/index.ts) + |-- IPC bridge (window.kpc) + |-- Action button grid (folder shortcuts, log viewers, reset/restart) + |-- Settings UI hooks (collapsible sections: swapper, matchmaker, chat, translator, accounts, advanced, userscripts) + |-- Userscript engine (metadata parser, execution, CSS injection, settings) + |-- Matchmaker IPC listener -> fetchGame() in matchmaker.ts + |-- Chat translator (Google Translate API, MutationObserver on #chatList) + |-- Exit button (exposes Krunker's native #clientExit element) + |-- Keybind capture dialog (Crankshaft-style) +``` + +### Source Files + +``` +src/ + main/ + index.ts Main process — window, IPC, session, ad blocking, keybinds + platform.ts OS detection, Chromium GPU/performance flags (per-platform) + config.ts electron-store schema, defaults, DEFAULT_KEYBINDS + client-ui.ts Injected CSS for settings panel, keybind dialog, matchmaker popup + swapper.ts Resource swapper — local asset overrides via custom protocol + userscripts.ts Userscript manager — filesystem scanning, tracker.json, preferences + discord-rpc.ts Discord Rich Presence via raw IPC socket + logger.ts File logging with daily rotation and 7-day retention + updater.ts Auto-updater — Gitea releases API check, download, installer launch + update-window.ts Update progress BrowserWindow with inline HTML + progress bar + preload/ + index.ts IPC bridge, settings UI hooks, keybind capture, matchmaker listener, alt manager + utils.ts Shared types (SavedConsole, KeybindDef), helpers (escapeHtml, genChatMsg) + matchmaker.ts Custom matchmaker — lobby fetch, filtering, popup UI + translator.ts Real-time chat translation via Google Translate API + userscripts.ts Userscript engine — metadata parser, execution, CSS injection, state +``` + +### Build System + +Two Vite configs build independent targets: + +| Config | Target | Output | Notes | +|--------|--------|--------|-------| +| `vite.main.config.ts` | Main process | `dist/main/index.js` (CJS) | Externalizes `electron`, `electron-store`, and Node builtins. Targets Node 20. | +| `vite.preload.config.ts` | Preload script | `dist/preload/index.js` (CJS) | Externalizes `electron` and `uuid`. Targets Node 20. | + +### Custom Electron Binary + +The patched Electron binary is managed by `scripts/download-electron.js`: + +- Downloads from Gitea release tag `electron-patched`, asset `electron-v42.0.0-nightly-patched-win32-x64.zip` +- Replaces `node_modules/electron/dist/` with the patched binary +- Runs automatically as a `postinstall` script during `npm install` +- Skips download if the version file already matches + +### Configuration (electron-store) + +Settings are persisted in `krunker-civilian-config.json` (OS-specific app data directory). Schema defined in `src/main/config.ts`: + +```typescript +interface AppConfig { + window: { width, height, x, y, maximized, fullscreen } + performance: { fpsUnlocked, hardwareAccel, gpuPreference } + game: { lastServer, socialTabBehaviour, joinAsSpectator } + swapper: { enabled, path } + userscripts: { enabled, path } + matchmaker: { enabled, regions, gamemodes, minPlayers, maxPlayers, minRemainingTime, openServerBrowser } + keybinds: { reload, newMatch, copyGameLink, joinFromClipboard, devTools, matchmaker, + matchmakerAccept, matchmakerCancel, pauseChat, fullscreenToggle } + ui: { showExitButton, deathscreenAnimation, hideMenuPopups } + discord: { enabled } + translator: { enabled, targetLanguage, showLanguageTag } + advanced: { removeUselessFeatures, gpuRasterizing, helpfulFlags, disableAccelerated2D, increaseLimits, + lowLatency, experimentalFlags, angleBackend } + accounts: SavedAccount[] + platform: { detectedOS, gpuBackend } +} +``` + +### Platform Handling + +| Feature | Windows | Linux | +|---------|---------|-------| +| Window chrome | Standard OS frame (`frame: true`) | Standard OS frame (`frame: true`) | +| GPU backend | D3D11 via ANGLE (default, configurable) | Default (+ `ozone-platform-hint auto` for Wayland) | +| GPU sandbox | Default | Disabled (AppImage FUSE + Mesa driver compat) | + +Common flags always applied: `disable-backgrounding-occluded-windows`, `ignore-gpu-blocklist`. FPS uncap flags (`disable-frame-rate-limit`, `disable-gpu-vsync`, `max-gum-fps=9999`) applied when enabled. The ANGLE backend is configurable (default D3D11 on Windows; options include OpenGL, Vulkan, D3D9, D3D11on12). Additional Chromium flags (GPU rasterization, useful features, low latency, etc.) are configurable via the Advanced settings panel. + +### Session & User-Agent + +- Uses `partition: 'persist:krunker'` for persistent login/settings across restarts +- User-Agent is cleaned to strip the app name identifier (keeps `Electron/` so Krunker enables its Client settings tab) + +## Features + +### Core Client +- Unlimited FPS via custom-patched Electron 42 build +- Ad blocking (network-level URL filter + CSS hiding) +- Resource swapper (replace game textures, sounds, models with local files) +- Custom matchmaker (filter lobbies by region, gamemode, player count, remaining time) +- Userscript system (Tampermonkey-style metadata, custom per-script settings, instant toggle via unload) +- Chat translator (real-time translation via Google Translate API with language tags) +- Exit button in Krunker's sidebar menu (exposes native `#clientExit` element) +- Selectable + pausable chat (always-on text selection, F10 to freeze auto-scroll for reading history) +- Configurable keybinds with Crankshaft-style rebinding dialog +- Configurable ANGLE backend (D3D11, OpenGL, Vulkan, D3D9, D3D11on12 — platform-filtered) +- Advanced Chromium flag settings (GPU rasterization, low latency, experimental features, and more) +- Client settings tab in Krunker's settings panel with collapsible sections +- Action button grid in settings (folder shortcuts, log viewers, reset/restart options) +- File-based logging (electron.log in userData/logs/, daily rotation with 7-day retention) +- Persistent game session, window state persistence, cookie consent auto-dismiss +- Auto-updater (Gitea releases API check, Setup.exe download with progress window, installer launch) +- Discord Rich Presence (game state, map, mode, player count) +- Death screen animation blocker (prevents CSS animation performance impact at uncapped FPS) +- Menu popup hider (hides store ads, stream containers, news popups) + +### Alt Manager (Account Switching) +- **Account storage** — Save multiple Krunker accounts (label, username, password) +- **Quick switch** — In-game "Accounts" menu item opens Krunker's native popup with saved accounts +- **Login automation** — Auto-logout + `loginOrRegister()` credential fill +- **Settings section** — Full account CRUD in the collapsed "Accounts" settings section +- **Credential obfuscation** — Credentials stored with character-shift encoding to prevent casual config reading + +## Known Constraints + +### MutationObserver Breaks WebGL on Main Frame + +A `MutationObserver` with `{ childList: true, subtree: true }` on Krunker's main frame DOM causes the WebGL 3D engine to hang indefinitely. Consent dismiss uses polling (`setInterval`) on the main frame instead. MutationObserver is safe in iframes and targeted elements like `#menuWindow`. + +### MutationObserver Infinite Loops (Chromium 94+) + +Observers that modify elements within their observed subtree can cause infinite loops. In Chromium 94+, even `textContent` assignments that don't change the value still create new text nodes (triggering `childList` mutations). Observers must disconnect before DOM modifications and reconnect after. + +### Synthetic Events Lack User Activation (Chromium 94+) + +Events created via `new MouseEvent()` / `new KeyboardEvent()` have `isTrusted: false` and do not provide user activation. Krunker ignores untrusted input events. The client uses `webContents.sendInputEvent()` via IPC to inject trusted input events from the main process. + +### Custom Electron Build Required + +The `--disable-frame-rate-limit` flag causes compositor input starvation on all Chromium versions 84+. The `ImplLatencyRecovery`/`MainLatencyRecovery` features that mitigated this were removed in Chromium 94. Our custom Electron 42 build patches the Chromium main thread scheduler to fairly interleave input and compositor work. See [Building from Source](#building-from-source) above and [`electron-build/BUILD.md`](electron-build/BUILD.md) for full instructions. + +### Uncapped FPS and CSS Animations + +At uncapped frame rates (600+ FPS), Krunker's CSS animations (e.g. death screen slide-in) force a layout recalculation on every frame, which can saturate the renderer main thread. The death screen animation blocker (`ui.deathscreenAnimation`) is enabled by default to prevent this. + +## Tech Stack + +- **Electron** 42 (Chromium 134, Node 24) — Custom-patched build for unlimited FPS +- **TypeScript** 5.7 — Type-safe source code +- **Vite** 6 — Fast bundler (2 build targets: main + preload) +- **electron-store** 8 — JSON config persistence (CJS) +- **uuid** 9 — Unique ID generation +- **electron-builder** 26 — Cross-platform packaging (NSIS, portable, AppImage, deb) + +## Project Structure + +``` +Krunker-Civilian-Client/ + src/ + main/ Main process source (10 modules) + preload/ Preload script source (5 modules) + dist/ Vite build output + main/ + preload/ + out/ electron-builder packaged output + build/ Build resources (icons, .desktop file) + scripts/ Build scripts (Electron patched binary download) + electron-build/ Custom Electron build instructions and patches + vite.main.config.ts + vite.preload.config.ts + electron-builder.yml + tsconfig.json + package.json +``` diff --git a/build/generate-icons.sh b/build/generate-icons.sh new file mode 100644 index 0000000..7162fa2 --- /dev/null +++ b/build/generate-icons.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Generate platform-specific icons from icon.svg +# Requires: imagemagick (convert) or inkscape + +# PNG for Linux (multiple sizes for best compatibility) +for size in 16 32 48 64 128 256 512; do + convert icon.svg -resize ${size}x${size} icon_${size}.png 2>/dev/null || \ + inkscape icon.svg -w $size -h $size -o icon_${size}.png 2>/dev/null +done + +# Copy 256px as the main Linux icon +cp icon_256.png icon.png + +# ICO for Windows (multi-resolution) +convert icon_16.png icon_32.png icon_48.png icon_64.png icon_128.png icon_256.png icon.ico 2>/dev/null + +echo "Icons generated. Place icon.png and icon.ico in the build/ directory." diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1ba807371c3ab493f1c0171351b2459e154f3135 GIT binary patch literal 47410 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 literal 0 HcmV?d00001 diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e46e523379c1718fae6232dc9e9b9f1240800e47 GIT binary patch literal 28227 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�RJ$;Qq#q8*SS*Vtu^ z%&9^Ifhq+spYjPfIix(#n{+Pp#+C#2*2gD$PdbkR8z4SDJ`sL`Y87l*d>@nP_2@v-*g^UpcwoCNuKF0bB$qSYTZiV7)j9j2!s#t77m zM67`N0Z>6i1dE6fq~}76!2H^*bChtn4=34Ff(UApURgLC=3{s*{`oHO7r6ph;}$=k zyVtSBSII7cJp`USKBcxG0ib@7#g7VPSS)@!YWJN%1h@uR{=fX?FR6~;*=L_k0YjA- z?*$^*?ucNMD}eRxB0QD7Zuz7mf=NRF?JWLs7w`jp?w%|@&_5E2Ld`C1Ej|{I={s0_ z{CX@tke~A1i!Z)-fsdcp>yL`iM~dG$A_DH`h=6PX?hdI=TmldQ@d6M5Q;G`e2wG(g zYGI1fQDIoBrW(KF!NZTMI)ceV05PgxWbw~%fxjfH+{-P#N?B{2SbPd(u=r=4b(URy z_0`G8hxy0J;&%%Xa7&Ef!3Q6-=bwK*A%c%T{_AO%n>K?JHSe9RHSizSF4W(~%g z#)G>$|9wTZ`pV-U;oW(H{L=*%KPiuPQGKG|SbTzeSb5TT#~yoZs@Esb7gKrsKT-Wr zxA=>p91(E7;%`RS2@uj}@YY*zrPLu!M+6H&1hJ?{RDcL{)Uuk4x!@8!8DazigRqm~7W$~w`nw7f?dhuoPf%w3Ensc!D`2DebcHAv~ zJ8JivK?5Ab13&~c9^n??BIJl521Z@Ev_k}6hswe?JdMcC!&llMg0UM_Hg4P&e^1ec znsFgNAdJ?&$szxWf~bD2oyCXo=$&WK25q@mdGh#JeeR;F*93P!7AseW$@VabC3c3i9Px@dV$Ycd1+d zgFDIN%i?Rceq=VP_6TCFUAs0l=n&My;`2EEeLi?|QZ0TvYIjb^77!nxiV#Hb_~VbK z#-k5E{4fnJ<#CZ(pA{nrh#?ElKfniV zp6v?Y60hdVyYt%m_%J7VdITG&kA=tD6XfHTygtGoNj__3@jIiujv${!fUr|H02&}j z$Snu~*8oL9EkV(BD~Sx9OM<+q~QtYMk!Y9;?4F z^zH#8MfD}D`FL$oc?dckc^pLc5q{u5sVo#jZow*xzj$hO&Ug+%r>YP)03|@ifG$K1 z6apzk5omxTxEV1^@g=CS^cJG$ z9zjRo5q<)D2tDJrvGzGUO>bQJ_+6)V=M*;!RKWOhlmS6PlmH3=#lTDg5eg)hEH_#s zf`%gkT!7C#NQgrCgWt}LG9svrk;Om8^Y=G;mtLfN7XQ2nJUNlWNAQ95MD&@k%PP>&AWXT&yepGZH!Jqf-nejMCc`S^pSY}n{LM1Ue7SV)Edw}4UR zxCK-ge*XFA=_uKKSXM`1S!H1(tU+Jz14>2)5!A-O;vd|uJ1^$(b044Rum)^m@lgO+ zd@Mgop|beng9s3IkN}8)S%ojX^pdS#zdj*?$P+O}&|w|HcF!7&93mKGH(*gvE}yyA zsl{(uxtI5sbAhq;2sl1H7N6aDC;-xYxB@|91YC2nI0SBq>wpN3 zKmPdS)`9X^jgEp~?OBK2hq1D7g$JEgYeJRjKHi167(|fX-zES7cnrh=5gRG#vfuPk%~v z1d&u(2q{Le1VnI`&m$;81U4vCFbD)tE3x+Gf~|i1V?BR=!%{4MwE7yY#Vu8OSp08) z``gVreJsAF@RV8nO7O=E5fJ!0zi5lETKy>eYS2OGX~_lhQ@KaIKF<~9 z@za}E7JuRp!KO`{Ql9~ph4dH@C?tib#-qHlFlG%Fi3pfk_&c9jSk$6!VZg~)C;+u& zMm4&hA1hot9_>Ex+Ca}-n!s|3zCLM=&|;#EWYknG=3C^mo@?- z(DkNjkOTLCT?W(7X!U`2Wj=oTfJUPd{0X6U z*LrSlF3lqV4Nz1B8lbR?A_821jXsZn`T|8LCf?_D|6yt1_? zNT8PrAlF*!+{gbL@5zs!y(`RbTa=HlvR%@8+~=`4Q6_egO`4BiG@*E@G%1;oMgby- z{T-UL8Z-yH#2%t2RmJ4{F6vbRU8SI0zVm1fG2I z$x=kHh|0n*y>{R>Gy9K@49s1JUEK@{_Tp;lJ!kxjuJ&Nx|Ky!|wd_Zv#T>1=^T^X9 z_=@Ud@!6omeJb~2V6Upxuc)8B1`w5l3?Lycz!Fpt0rx=!D1z8{RMLH@x|sA-kR3V> z|7Q;lZV9rJ{?&$10jR=H_sJ@6hPere5d99;N zrt0B4jR|lrv1~9XiWa1WGNyBKQmHn7h55X5$@$gTcS_BPM{x_kVz8(q5F*GY6uAu4 z3JQP@blIVJ=-J>3bS)Op3k48WyICGD+So?_ZnwJP{xH$<#)EG7dz?Ylyft-iCAEO2! z1uzongyAQ#Ci|9KZb^Q=NL5(*ti4L`+Y{%CnFb&$s@OQ!!~i&-owxwhQfNyt@4I+-2(1Zq@>HO99At5zL?UR>&i=y8e{z zi@`gJdhq%2lYmH(V%L@bbCc+fP?>w!N7_-Bk^e~g9Wd{^B@4` z7E(ME+*@QM{?iJVr6avYKyAcvmcWvzn<_oNm-?aCg`8%3O{}QG%5Wl>x{Hq_tHa5N zj5;Y*XuY7AYFZ484@D~#{9htPzSSr?@ix2;&%^u30Ds3`gdk51m!WkoQ!BbDzui;- zxyD-Ouw|8=%LSQKA1OYXceFJZn99kRq^h%iQXa4>J1Xif$%TctRk#Mv!~4iUTp6Fq zNi7T$S(hW~z5QOja_Uxvi74|lR`eqYiV*e31>mFz0%<5?MHP(`!PBb6fY-SX?;``q zf+#}(mQ5JSSJw|!3_vvNExw0%B(o99eVeZ%H%%~v+%orL3Z+s26^$F!0&jD`^YA`0 zkQG=p3*=mOmIZmr>jCyE6VNLJQ1?b4j?(B6z7Y9pRYt{8%oSBMj?~C%1>k*TpkNa5 zqUC9j3LxhHp>1BixWOZqYV>I?VS@;WP^`if)))n?0#Dtey^W<*tIoPdk+

cplzI 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 + + KP + diff --git a/build/krunker-civilian-client.desktop b/build/krunker-civilian-client.desktop new file mode 100644 index 0000000..bade7d3 --- /dev/null +++ b/build/krunker-civilian-client.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Krunker Civilian Client +Comment=Cross-platform Krunker game client +Exec=krunker-civilian-client %U +Icon=krunker-civilian-client +Type=Application +Categories=Game;ActionGame; +Keywords=krunker;fps;game; +StartupWMClass=krunker-civilian-client +MimeType=x-scheme-handler/krunker; diff --git a/electron-build/BUILD.md b/electron-build/BUILD.md new file mode 100644 index 0000000..2278f4b --- /dev/null +++ b/electron-build/BUILD.md @@ -0,0 +1,193 @@ +# Building Patched Electron 42 (Input Priority Fix) + +This builds a custom Electron with a one-line Chromium patch that fixes input starvation ("aim freeze") when `--disable-frame-rate-limit` is active. Without this patch, uncapped frame rates cause 50-300ms input delays in GPU-intensive applications like browser FPS games. + +## The Problem + +Chromium's main thread scheduler gives input tasks `kHighestPriority`. At uncapped frame rates, the compositor floods the task queue and input events get starved — your mouse movements are delayed by up to 300ms, then snap to catch up. Chromium 87-93 had `ImplLatencyRecovery`/`MainLatencyRecovery` features that mitigated this, but they were removed in Chromium 94. + +## The Fix + +One line in `main_thread_scheduler_impl.cc` — demote input tasks from `kHighestPriority` to `kNormalPriority`, allowing the scheduler's anti-starvation logic to fairly interleave input and compositor work. + +## Prerequisites + +- **OS**: Windows 10/11 x64 (builds on Linux too, adjust paths accordingly) +- **Disk**: ~100 GB free (Chromium source + build artifacts) +- **RAM**: 16 GB minimum, 32 GB recommended +- **Visual Studio 2022** with "Desktop development with C++" workload and Windows 11 SDK +- **Git** and **Python 3.8+** on PATH + +## Step 1: Install depot_tools + +```powershell +cd C:\ +git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +# Add C:\depot_tools to the FRONT of your system PATH +# Then open a NEW terminal +``` + +Verify: `gclient --version` should print a version. + +## Step 2: Check out Electron source + +```powershell +mkdir C:\electron && cd C:\electron + +# Create gclient config for Electron +gclient config --name "src/electron" --unmanaged https://github.com/nicedayzhu/electron.git@v42.0.0-nightly.20260227 +``` + +> **Note**: Replace the repo URL with your fork if you've pushed the patch there. The `@v42.0.0-nightly.20260227` pins the exact nightly tag. + +```powershell +# Sync all dependencies (~40-60 GB download, takes a while) +gclient sync --with_branch_heads --with_tags +``` + +## Step 3: Apply the patch + +```powershell +cd C:\electron\src + +# Apply the patch file +git apply --directory=. path\to\input-priority-fix.patch +``` + +Or make the edit manually — in `third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc`, find: + +```cpp +case MainThreadTaskQueue::QueueTraits::PrioritisationType::kInput: + return TaskPriority::kHighestPriority; +``` + +Change `kHighestPriority` to `kNormalPriority`. + +## Step 4: Configure the build + +### Release build (optimized, for distribution): + +```powershell +cd C:\electron\src + +# Create build directory +gn gen out/Release + +# Copy the release args +copy path\to\args.release.gn out\Release\args.gn + +# Regenerate build files with the new args +gn gen out/Release +``` + +Contents of `args.release.gn`: +```gn +import("//electron/build/args/release.gn") +is_official_build = true +use_remoteexec = false +use_reclient = false +``` + +### Testing build (faster compile, for development): + +```powershell +gn gen out/Testing +``` + +Write to `out/Testing/args.gn`: +```gn +import("//electron/build/args/testing.gn") +use_remoteexec = false +use_reclient = false +``` + +Then: `gn gen out/Testing` + +## Step 5: Build + +```powershell +cd C:\electron\src + +# Release build (~2-4 hours depending on CPU) +ninja -C out/Release electron + +# OR Testing build (~1-2 hours, less optimization) +ninja -C out/Testing electron +``` + +> **Tip**: Use `ninja -C out/Release electron -j N` to limit parallelism if you're running out of RAM (where N = number of parallel jobs, try RAM_GB / 2). + +## Step 6: Create distributable zip + +```powershell +cd C:\electron\src + +# Generate the electron dist zip +python3 electron/script/zip_manifests/create-dist-zip.py out/Release + +# Or use electron's strip-binaries + create-dist tooling: +ninja -C out/Release electron:dist_zip +``` + +The output zip will be at `out/Release/dist.zip` (or similar). This contains `electron.exe` and all required DLLs/resources. + +## Step 7: Verify + +Extract the zip and test with a minimal app: + +```powershell +# Create a test directory +mkdir test-app +``` + +Create `test-app/package.json`: +```json +{ "name": "test", "version": "1.0.0", "main": "main.js" } +``` + +Create `test-app/main.js`: +```js +const { app, BrowserWindow } = require('electron'); +app.commandLine.appendSwitch('disable-frame-rate-limit'); +app.commandLine.appendSwitch('disable-gpu-vsync'); +app.whenReady().then(() => { + const win = new BrowserWindow({ width: 1280, height: 720 }); + win.loadURL('https://krunker.io'); + win.webContents.on('did-finish-load', () => { + console.log('Electron:', process.versions.electron); + console.log('Chrome:', process.versions.chrome); + }); +}); +``` + +Run it: +```powershell +path\to\electron.exe test-app +``` + +If Krunker loads at uncapped FPS with no aim freeze, the build is good. + +## Using the patched Electron in a project + +To use this as the Electron binary in an npm project: + +```powershell +# Set environment variable to point to your custom build +set ELECTRON_OVERRIDE_DIST_PATH=C:\path\to\extracted\electron-dist + +# Then run your Electron app normally +npm start +``` + +Or replace the contents of `node_modules/electron/dist/` with the extracted zip contents. + +## Build time estimates + +| Build type | CPU | Approx. time | +|---|---|---| +| Testing | 8-core | ~1-2 hours | +| Testing | 16-core | ~30-60 min | +| Release | 8-core | ~3-5 hours | +| Release | 16-core | ~1.5-3 hours | + +Release builds are significantly slower due to LTO (Link-Time Optimization) which does a whole-program optimization pass. diff --git a/electron-build/args.release.gn b/electron-build/args.release.gn new file mode 100644 index 0000000..2a85579 --- /dev/null +++ b/electron-build/args.release.gn @@ -0,0 +1,8 @@ +import("//electron/build/args/release.gn") + +# Full optimization (LTO, minimal symbols, etc.) +is_official_build = true + +# Not using Google's remote build infrastructure +use_remoteexec = false +use_reclient = false diff --git a/electron-build/input-priority-fix.patch b/electron-build/input-priority-fix.patch new file mode 100644 index 0000000..b50b132 --- /dev/null +++ b/electron-build/input-priority-fix.patch @@ -0,0 +1,29 @@ +From: KPD Client +Subject: [PATCH] Fix input starvation when frame rate limit is disabled + +Chromium's main thread scheduler assigns kHighestPriority to input tasks, +which starves the compositor when --disable-frame-rate-limit is active. +At uncapped frame rates (300+ FPS), the compositor floods the task queue +and input events get delayed 50-300ms, causing "aim freeze" in games. + +Demoting input tasks to kNormalPriority allows the scheduler's built-in +anti-starvation logic to fairly interleave input and compositor work. + +Benchmarked via CDP Input.dispatchMouseEvent: +- p99 latency: 97ms -> 34ms +- Max latency: 308ms -> 38ms +- Events >50ms: 8.6% -> 0% +- Frames rendered: +21% +- Mouse events processed: +9% + +--- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc ++++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc +@@ -2354,7 +2354,7 @@ + case MainThreadTaskQueue::QueueTraits::PrioritisationType::kCompositor: + return main_thread_only().compositor_priority; + case MainThreadTaskQueue::QueueTraits::PrioritisationType::kInput: +- return TaskPriority::kHighestPriority; ++ return TaskPriority::kNormalPriority; + case MainThreadTaskQueue::QueueTraits::PrioritisationType::kBestEffort: + return TaskPriority::kBestEffortPriority; + case MainThreadTaskQueue::QueueTraits::PrioritisationType::kRegular: diff --git a/electron-builder.yml b/electron-builder.yml new file mode 100644 index 0000000..a934f3b --- /dev/null +++ b/electron-builder.yml @@ -0,0 +1,72 @@ +appId: com.krunkercivilian.client +productName: Krunker Civilian Client +electronDist: node_modules/electron/dist +directories: + output: out + buildResources: build +files: + - dist/**/* + - node_modules/electron-store/**/* + - node_modules/conf/**/* + - node_modules/dot-prop/**/* + - node_modules/type-fest/**/* + - node_modules/pkg-up/**/* + - node_modules/find-up/**/* + - node_modules/locate-path/**/* + - node_modules/p-locate/**/* + - node_modules/p-limit/**/* + - node_modules/yocto-queue/**/* + - node_modules/path-exists/**/* + - node_modules/env-paths/**/* + - node_modules/json-schema-typed/**/* + - node_modules/ajv/**/* + - node_modules/ajv-formats/**/* + - node_modules/atomically/**/* + - node_modules/debounce-fn/**/* + - node_modules/mimic-fn/**/* + - node_modules/semver/**/* + - node_modules/onetime/**/* + - node_modules/uuid/**/* + - "!node_modules/**/*.ts" + - "!node_modules/**/*.map" +asar: true + +win: + target: + - target: nsis + arch: [x64] + - target: portable + arch: [x64] + icon: build/icon.ico + +nsis: + oneClick: false + allowToChangeInstallationDirectory: true + createDesktopShortcut: true + createStartMenuShortcut: true + artifactName: "${productName}-${version}-Setup.${ext}" + +portable: + artifactName: "${productName}-${version}-Portable.${ext}" + +linux: + target: + - target: AppImage + arch: [x64] + - target: deb + arch: [x64] + icon: build/icon.png + category: Game + artifactName: "${productName}-${version}-linux-${arch}.${ext}" + desktop: + entry: + Name: Krunker Civilian Client + Comment: Cross-platform Krunker game client + Categories: Game;ActionGame; + Keywords: krunker;fps;game; + StartupWMClass: krunker-civilian-client + +publish: + provider: github + owner: krunker-civilian + repo: krunker-civilian-client diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..019b78d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6272 @@ +{ + "name": "krunker-civilian-client", + "version": "4.1.22", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "krunker-civilian-client", + "version": "4.1.22", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "electron-store": "^8.2.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "electron": "npm:electron-nightly@42.0.0-nightly.20260227", + "electron-builder": "^26.0.0", + "rimraf": "^6.0.1", + "typescript": "^5.7.0", + "vite": "^6.0.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, + "license": "MIT", + "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/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/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, + "license": "MIT", + "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/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "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, + "license": "MIT", + "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/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "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" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "peer": 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-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "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": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/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==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "Python-2.0" + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "license": "MIT", + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/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" + } + ], + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "license": "MIT", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/conf/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "optional": true + }, + "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, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": 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, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.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, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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, + "license": "MIT", + "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/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "name": "electron-nightly", + "version": "42.0.0-nightly.20260227", + "resolved": "https://registry.npmjs.org/electron-nightly/-/electron-nightly-42.0.0-nightly.20260227.tgz", + "integrity": "sha512-aZ0+csF80+4qwX20oxGiaaTV667l2RPcSvXceM3zxUGvMBP2Nc9nNumicPMBlslp85uAyx9qhG/0+FQ543f5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "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-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "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, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-store": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.2.0.tgz", + "integrity": "sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==", + "license": "MIT", + "dependencies": { + "conf": "^10.2.0", + "type-fest": "^2.17.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.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==", + "dev": true, + "license": "BSD-2-Clause", + "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" + ], + "license": "MIT", + "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==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "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/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "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/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "license": "BSD-2-Clause" + }, + "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==", + "dev": true, + "license": "ISC", + "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, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", + "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "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": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "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==", + "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" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/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==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.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, + "license": "MIT", + "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-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "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, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "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, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.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, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "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/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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, + "license": "MIT", + "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-cjs": { + "name": "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, + "license": "MIT", + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..59807ce --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "krunker-civilian-client", + "version": "4.1.22", + "description": "Cross-platform Krunker game client", + "main": "dist/main/index.js", + "homepage": "https://gitea.crjlab.net/bigjakk/krunker-civilian-client", + "author": "Krunker Civilian Client ", + "license": "MIT", + "scripts": { + "postinstall": "node scripts/download-electron.js", + "dev": "vite build --mode development --config vite.main.config.ts && vite build --mode development --config vite.preload.config.ts && electron .", + "build:main": "vite build --config vite.main.config.ts", + "build:preload": "vite build --config vite.preload.config.ts", + "build": "npm run build:main && npm run build:preload", + "start": "npm run build && electron .", + "download-electron": "node scripts/download-electron.js", + "dist:win": "npm run build && electron-builder --win", + "dist:linux": "npm run build && electron-builder --linux", + "dist:all": "npm run build && electron-builder --win --linux", + "clean": "rimraf dist out" + }, + "dependencies": { + "electron-store": "^8.2.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "electron": "npm:electron-nightly@42.0.0-nightly.20260227", + "electron-builder": "^26.0.0", + "rimraf": "^6.0.1", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/scripts/download-electron.js b/scripts/download-electron.js new file mode 100644 index 0000000..d490e5a --- /dev/null +++ b/scripts/download-electron.js @@ -0,0 +1,205 @@ +'use strict'; + +/** + * Downloads the patched Electron build and extracts it into node_modules/electron/dist/. + * + * The patched Electron fixes input starvation ("aim freeze") when --disable-frame-rate-limit + * is active on modern Chromium. Without this, uncapped FPS causes 50-300ms input delays. + * + * The zip is hosted as a release asset on the same Gitea repo. The script checks the + * local version file to skip re-downloading if already present. + * + * Usage: + * node scripts/download-electron.js # download if needed + * node scripts/download-electron.js --force # re-download even if present + */ + +const https = require('https'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// ── Configuration ────────────────────────────────────────────────────────── +const ELECTRON_VERSION = '42.0.0-nightly.20260227'; +const ASSET_NAME = 'electron-v42.0.0-nightly-patched-win32-x64.zip'; +const GITEA_BASE = 'https://gitea.crjlab.net'; +const REPO = 'bigjakk/KPC'; +// The release tag that holds the patched Electron zip. +// Upload the zip as an asset to this release on Gitea. +const RELEASE_TAG = 'electron-patched'; + +// On Windows, overwrite the npm-installed Electron with our patched build. +// On Linux/macOS (CI cross-compilation), extract to a separate dist-win/ directory +// so the npm-installed platform-native Electron stays in dist/ for bytenode compilation. +const IS_WIN = process.platform === 'win32'; +const ELECTRON_DIST = IS_WIN + ? path.resolve(__dirname, '..', 'node_modules', 'electron', 'dist') + : path.resolve(__dirname, '..', 'node_modules', 'electron', 'dist-win'); +const VERSION_FILE = path.join(ELECTRON_DIST, 'version'); +// Separate marker file to distinguish patched from stock electron-nightly. +// Both have the same version string, so VERSION_FILE alone is not sufficient. +const PATCHED_MARKER = path.join(ELECTRON_DIST, '.patched'); +const TEMP_ZIP = path.join(ELECTRON_DIST, '..', '_electron-patched.zip'); + +// ── Helpers ──────────────────────────────────────────────────────────────── + +function get(url) { + const lib = url.startsWith('https') ? https : http; + return new Promise((resolve, reject) => { + lib.get(url, { headers: { 'User-Agent': 'KCC-Build' } }, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + get(res.headers.location).then(resolve, reject); + res.resume(); + return; + } + if (res.statusCode !== 200) { + res.resume(); + reject(new Error(`HTTP ${res.statusCode} for ${url}`)); + return; + } + resolve(res); + }).on('error', reject); + }); +} + +function downloadToFile(url, dest) { + return new Promise(async (resolve, reject) => { + try { + const res = await get(url); + const total = parseInt(res.headers['content-length'] || '0', 10); + let downloaded = 0; + + const file = fs.createWriteStream(dest); + res.on('data', (chunk) => { + downloaded += chunk.length; + if (total > 0) { + const pct = ((downloaded / total) * 100).toFixed(1); + const mb = (downloaded / 1048576).toFixed(1); + const totalMb = (total / 1048576).toFixed(1); + process.stdout.write(`\r Downloading: ${pct}% (${mb}/${totalMb} MB)`); + } + }); + res.pipe(file); + file.on('finish', () => { + file.close(); + process.stdout.write('\n'); + resolve(); + }); + file.on('error', (err) => { + fs.unlinkSync(dest); + reject(err); + }); + } catch (err) { + reject(err); + } + }); +} + +async function getAssetUrl() { + const apiUrl = `${GITEA_BASE}/api/v1/repos/${REPO}/releases/tags/${RELEASE_TAG}`; + const res = await get(apiUrl); + const body = await new Promise((resolve, reject) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => resolve(data)); + res.on('error', reject); + }); + + const release = JSON.parse(body); + const asset = release.assets.find((a) => a.name === ASSET_NAME); + if (!asset) { + const names = release.assets.map((a) => a.name).join(', '); + throw new Error( + `Asset "${ASSET_NAME}" not found in release "${RELEASE_TAG}".\n` + + ` Available assets: ${names || '(none)'}\n` + + ` Upload the patched Electron zip to: ${GITEA_BASE}/${REPO}/releases/tag/${RELEASE_TAG}` + ); + } + + // Gitea API returns browser_download_url for direct download + return asset.browser_download_url; +} + +function extractZip(zipPath, destDir) { + // Use PowerShell on Windows, unzip on Linux/macOS + if (process.platform === 'win32') { + execSync( + `powershell -NoProfile -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${destDir}'"`, + { stdio: 'inherit' } + ); + } else { + execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' }); + } +} + +// ── Main ─────────────────────────────────────────────────────────────────── + +async function main() { + const force = process.argv.includes('--force'); + + // Check if patched version is already installed. + // The .patched marker distinguishes our build from stock electron-nightly + // (both share the same version string). + if (!force && fs.existsSync(PATCHED_MARKER)) { + const installed = fs.readFileSync(PATCHED_MARKER, 'utf8').trim(); + if (installed === ELECTRON_VERSION) { + console.log(` Patched Electron ${ELECTRON_VERSION} already installed, skipping`); + console.log(' (use --force to re-download)'); + return; + } + console.log(` Installed: ${installed}, need: ${ELECTRON_VERSION}`); + } + + // Resolve download URL from Gitea release + console.log(` Fetching release info for "${RELEASE_TAG}"...`); + const url = await getAssetUrl(); + console.log(` Asset URL: ${url}`); + + // Download + await downloadToFile(url, TEMP_ZIP); + const zipSize = (fs.statSync(TEMP_ZIP).size / 1048576).toFixed(1); + console.log(` Downloaded: ${zipSize} MB`); + + // Clear existing target dir and extract + console.log(` Extracting to ${path.relative(path.resolve(__dirname, '..'), ELECTRON_DIST)}/...`); + if (fs.existsSync(ELECTRON_DIST)) { + fs.rmSync(ELECTRON_DIST, { recursive: true, force: true }); + } + fs.mkdirSync(ELECTRON_DIST, { recursive: true }); + extractZip(TEMP_ZIP, ELECTRON_DIST); + + // Clean up temp zip + fs.unlinkSync(TEMP_ZIP); + + // Write path.txt so the electron package's lazy downloader (index.js) + // considers the binary already installed and doesn't re-download stock. + const platformExe = process.platform === 'win32' ? 'electron.exe' : 'electron'; + fs.writeFileSync(path.join(ELECTRON_DIST, '..', 'path.txt'), platformExe); + + // Write marker and verify + if (fs.existsSync(VERSION_FILE)) { + const ver = fs.readFileSync(VERSION_FILE, 'utf8').trim(); + fs.writeFileSync(PATCHED_MARKER, ver); + console.log(` Installed patched Electron ${ver}`); + } else { + console.log(' Warning: version file not found after extraction'); + } +} + +console.log('[KCC] Setting up patched Electron...'); +main().then(() => { + console.log('[KCC] Patched Electron ready.'); +}).catch((err) => { + console.error('[KCC] Electron download failed:', err.message); + console.error(''); + console.error(' If this is your first time building, you need the patched Electron zip'); + console.error(` uploaded as a release asset on ${GITEA_BASE}/${REPO}`); + console.error(''); + console.error(' 1. Go to: ' + GITEA_BASE + '/' + REPO + '/releases/new'); + console.error(` 2. Create a release with tag: ${RELEASE_TAG}`); + console.error(` 3. Upload: ${ASSET_NAME}`); + console.error(''); + console.error(' See electron-build/BUILD.md for how to build Electron from source.'); + process.exit(1); +}); diff --git a/src/main/client-ui.ts b/src/main/client-ui.ts new file mode 100644 index 0000000..d12e360 --- /dev/null +++ b/src/main/client-ui.ts @@ -0,0 +1,587 @@ +// ── Injected CSS for client settings in Krunker's settings panel ── +export const CLIENT_SETTINGS_CSS = ` +:root { + /* ── Surfaces ── */ + --kpc-surface-card: rgba(255,255,255,0.04); + --kpc-surface-input: rgba(255,255,255,0.08); + --kpc-surface-hover: rgba(255,255,255,0.1); + --kpc-surface-hover-strong: rgba(255,255,255,0.15); + --kpc-surface-dialog: #1a1a1a; + --kpc-surface-raised: #212121; + + /* ── Text ── */ + --kpc-text-primary: rgba(255,255,255,0.9); + --kpc-text-secondary: rgba(255,255,255,0.7); + --kpc-text-muted: rgba(255,255,255,0.5); + --kpc-text-faint: rgba(255,255,255,0.35); + --kpc-text-dim: rgba(255,255,255,0.3); + --kpc-text-info: #888; + + /* ── Borders ── */ + --kpc-border-subtle: rgba(255,255,255,0.06); + --kpc-border-default: rgba(255,255,255,0.1); + --kpc-border-medium: rgba(255,255,255,0.15); + --kpc-border-focus: rgba(255,255,255,0.35); + + /* ── Accents ── */ + --kpc-green: #4CAF50; + --kpc-green-hover: #66bb6a; + --kpc-red: #ef5350; + --kpc-red-hover: #e57373; + --kpc-blue: #42a5f5; + --kpc-blue-hover: #64b5f6; + --kpc-orange: #ff9800; + --kpc-orange-hover: #ffb74d; + --kpc-yellow: #ffc107; + --kpc-magenta: #fc03ec; + + /* ── Controls ── */ + --kpc-toggle-off: rgba(255,255,255,0.12); + + /* ── Z-index layers ── */ + --kpc-z-notification: 100000; + --kpc-z-overlay: 10000000; + --kpc-z-popup: 10000001; + +} +/* ── Crankshaft-style settings (Krunker-native classes) ── */ + +.kpc-settings .settName, +.kpc-settings .settName .setting-title { + color: rgba(255,255,255,.6) !important; +} + +.kpc-settings .settName { + display: grid; + grid-auto-columns: 1fr; + grid-template-columns: 0fr 1fr 0fr; + grid-template-areas: + "icon title input" + "desc desc desc"; + grid-template-rows: 0fr min-content; + align-items: center; +} +.kpc-settings .settName.multisel { + grid-template-rows: min-content 1fr; + grid-template-columns: 0fr 1fr; + grid-template-areas: + "icon title" + "input input"; +} +.kpc-settings .settName.has-button { + grid-template-areas: + "icon title button input" + "desc desc desc desc"; + grid-template-columns: 0fr 1fr min-content 0fr; +} +.kpc-settings .settName.has-button .settingsBtn { + grid-area: button; + margin: 0 .5rem; +} + +.kpc-settings .settName.kpc-button-holder { + grid-template-columns: 1fr; + grid-auto-columns: min-content; + column-gap: 0.25rem; + grid-template-areas: unset; + grid-template-rows: 0fr; + grid-auto-flow: column; +} +.kpc-settings .kpc-button-holder .buttons-title, .material-icons { color: inherit; } +.kpc-settings .kpc-button-holder .settingsBtn, +.kpc-settings .settName.has-button .settingsBtn { + width: max-content; +} + +/* type: num */ +.kpc-settings .settName.num .setting-input-wrapper { + display: flex; +} +.kpc-settings .settName.num .setting-input-wrapper .slidecontainer { + margin-top: -8px; +} + +/* type: multisel */ +.kpc-multisel-parent { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-auto-rows: 1fr; + gap: .25rem; + background: #232323; + border-radius: 10px; + margin-top: 0.8rem; +} +.kpc-multisel-parent label.hostOpt { + width: 100%; + margin: 0; + box-sizing: border-box; +} + +.kpc-settings .settName.multisel label { + font-size: 1.1rem; +} +.kpc-settings .settName.multisel input { + margin-left: .25rem; +} + +/* general settings */ +.kpc-settings .settName .setting-title { + grid-area: title; +} + +.kpc-settings .settName .s-update:disabled, +.kpc-settings .settName .s-update:disabled+.slider.round { + opacity: 0.5; + pointer-events: none; +} + +.kpc-settings .setting .switch { + box-sizing: border-box; +} + +.kpc-settings .setting .desc-icon { + grid-area: icon; + cursor: pointer; + font-size: 1rem; + width: 2.2rem; + height: 2.2rem; + line-height: 2.2rem; + border-radius: 5px !important; + color: #969696; + background-color: rgba(99, 99, 99, 0.16); + border: 2px solid rgba(78, 78, 78, 0.81); + margin-right: 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.kpc-settings .setting .desc-icon.instant { + background-color: rgba(1, 89, 220, 0.16); + border: 2px solid rgba(3, 133, 255, 0.81); +} + +.kpc-settings .setting .desc-icon.instant svg path { + color: #0385ff; + fill: currentColor; +} + +.kpc-settings .setting.settName .inputGrey2, +.kpc-settings .setting.settName .switch, +.kpc-settings .setting.settName .kpc-multisel-parent, +.kpc-settings .setting.settName .setting-input-wrapper, +.kpc-settings .setting.settName .keyIcon { + grid-area: input; +} + +.kpc-settings .setting.safety-1 .desc-icon, +.kpc-settings .setting .desc-icon.refresh-icon, +.kpc-settings .setting .desc-icon.restart-icon { + background-color: rgba(99, 99, 99, 0.16); + border: 2px solid rgba(78, 78, 78, 0.81); +} + +.kpc-settings .setting.safety-1 .desc-icon svg path, +.kpc-settings .setting .desc-icon.refresh-icon svg path, +.kpc-settings .setting .desc-icon.restart-icon svg path { + color: #969696; + fill: currentColor; +} + +.kpc-settings .setting.safety-2 .desc-icon { + background-color: rgba(220, 180, 1, 0.16); + border: 2px solid rgba(241, 186, 6, 0.81); +} + +.kpc-settings .setting.safety-2 .desc-icon svg path { + color: #ffd903; + fill: currentColor; +} + +.kpc-settings .setting.safety-3 .desc-icon { + background-color: rgba(220, 118, 1, 0.16); + border: 2px solid rgba(241, 131, 6, 0.81); +} + +.kpc-settings .setting.safety-3 .desc-icon svg path { + color: #ff9203; + fill: currentColor; +} + +.kpc-settings .setting.safety-4 .desc-icon { + background-color: rgba(220, 17, 1, 0.16); + border: 2px solid rgba(239, 6, 6, 0.81); +} + +.kpc-settings .setting.safety-4 .desc-icon svg path { + color: #ff0303; + fill: currentColor; +} + +.desc-icon { + position: relative; +} + +.setting-desc-new { + display: block; + width: fit-content; + max-width: 50ch; + line-height: 30px; + font-size: 15px; + letter-spacing: 0.5px; + word-wrap: break-word; + color: rgba(255, 255, 255, 0.4) !important; + overflow: hidden; + max-height: 500px; + margin-top: 6px; + grid-area: desc; +} + +.setting-desc-new a { + font-size: inherit !important; + font-family: inherit !important; +} + +.setting-category-collapsed { + display: none; +} + +/* keybind display */ +.keyIcon.kpc-keyIcon:hover { + transform: scale(1.25); + cursor: pointer; +} + +.keyIcon.kpc-keyIcon { + display: inline-block; + transition: 0s; +} + +/* ── KPC action button grid ── */ +.kpc-action-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; + padding: 0 12px 12px; +} +.kpc-action-btn { + background: var(--kpc-surface-card); + color: var(--kpc-text-primary); + border: 2px solid var(--kpc-border-medium); + padding: 10px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + text-align: center; + transition: background 0.15s, border-color 0.15s; + user-select: none; +} +.kpc-action-btn:hover { + background: var(--kpc-surface-hover); + border-color: var(--kpc-border-focus); +} +.kpc-action-btn:active { + transform: scale(0.97); +} +.kpc-action-btn.full { + grid-column: 1 / -1; +} +.kpc-action-btn.kpc-ab-purple { border-color: #ab47bc; } +.kpc-action-btn.kpc-ab-purple:hover { border-color: #ce93d8; } +.kpc-action-btn.kpc-ab-cyan { border-color: #00bcd4; } +.kpc-action-btn.kpc-ab-cyan:hover { border-color: #4dd0e1; } +.kpc-action-btn.kpc-ab-pink { border-color: #ec407a; } +.kpc-action-btn.kpc-ab-pink:hover { border-color: #f48fb1; } +.kpc-action-btn.kpc-ab-red { border-color: var(--kpc-red); } +.kpc-action-btn.kpc-ab-red:hover { border-color: var(--kpc-red-hover); } +.kpc-action-btn.kpc-ab-orange { border-color: var(--kpc-orange); } +.kpc-action-btn.kpc-ab-orange:hover { border-color: var(--kpc-orange-hover); } + +/* floating toasts css that is required */ +.kpc-holder-update { + position: absolute; + font-size: 1.125rem !important; + color: rgba(255, 255, 255, 0.7); + display: block !important; + top: 20px; + left: 20px; + background-color: black; + padding: 1rem; + border-radius: 0.5rem; + width: max-content; + z-index: 10; +} + +/* settings refresh popup */ +.refresh-popup { + height: min-content; + left: 50%; + transform: translateX(-50%); + color: rgba(255,255,255,0.6) +} +.refresh-popup span { + display: flex; + align-items: center; + column-gap: 0.5rem; + color: rgba(255,255,255,0.6); +} +.refresh-popup, +.refresh-popup span, +.refresh-popup a { + vertical-align: middle; + font-size: .8rem; + line-height: .8rem; + z-index: 12; +} +.refresh-popup svg { fill: rgba(255,255,255,0.6); } +.refresh-popup code { + color: white; + font-size: 1.2rem; + line-height: 1.2rem; + font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; + background-color: #232323; + padding: 0.08rem 0.4rem; + border-radius: 3px; + border: 2px solid #333333 +} +/* ── Keybind capture dialog ── */ +.kpc-keybind-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: var(--kpc-z-overlay); + background: rgba(0,0,0,0.7); + display: flex; + align-items: center; + justify-content: center; +} +.kpc-keybind-dialog { + background: var(--kpc-surface-dialog); + border: 1px solid var(--kpc-border-medium); + border-radius: 10px; + padding: 24px 32px; + min-width: 400px; + position: relative; +} +.kpc-keybind-dialog-title { + color: var(--kpc-text-primary); + font-size: 18px; + margin-bottom: 6px; +} +.kpc-keybind-dialog-sub { + color: var(--kpc-text-muted); + font-size: 13px; + margin-bottom: 16px; +} +.kpc-keybind-dialog-sub code { + color: #64b5f6; +} +.kpc-keybind-dialog-modifiers { + display: flex; + gap: 8px; + font-size: 14px; +} +.kpc-keybind-modifier { + background: var(--kpc-surface-raised); + color: var(--kpc-text-faint); + flex: 1; + text-align: center; + padding: 10px 0; + border-radius: 6px; + transition: background 0.15s, color 0.15s; +} +.kpc-keybind-modifier.active { + background: #1976d2; + color: #fff; +} +.kpc-keybind-dialog-cancel { + position: absolute; + top: 12px; + right: 16px; + color: #64b5f6; + cursor: pointer; + font-size: 14px; +} +.kpc-keybind-dialog-cancel:hover { + text-decoration: underline; +} +/* ── Preserved: color input, userscript meta ── */ +.kpc-color-input { + width: 36px; + height: 28px; + border: 1px solid var(--kpc-border-default); + border-radius: 4px; + background: transparent; + cursor: pointer; + padding: 0; + flex-shrink: 0; +} +.kpc-color-input::-webkit-color-swatch-wrapper { + padding: 2px; +} +.kpc-color-input::-webkit-color-swatch { + border: none; + border-radius: 2px; +} +.kpc-us-meta { + color: var(--kpc-text-dim); + font-size: 11px; + margin-top: 2px; +} +.kpc-us-settings { + padding: 4px 0 4px 20px; +} +#chatList, #chatList * { + user-select: text !important; + cursor: text; +} +#chatList.kpc-chat-paused { + border-left: 2px solid var(--kpc-yellow); +} +`; + + +// ── Matchmaker popup CSS + settings extras (injected separately) ── +export const MATCHMAKER_SETTINGS_CSS = ` +@keyframes matchmakerPopupSlideDown { + 0% { transform: translate(-50%, -500%); } + 100% { transform: translate(-50%, 0%); } +} +.onGame #matchmakerPopupContainer { + opacity: 0 !important; +} +#matchmakerPopupContainer { + position: absolute; + top: 10em; + left: 50%; + z-index: var(--kpc-z-popup); + box-sizing: border-box; + width: 35em; + aspect-ratio: 2.5/1; + border-radius: 1.2em; + overflow: hidden; + background-size: 100% 100%; + pointer-events: all; + background-color: var(--kpc-surface-raised); + animation: matchmakerPopupSlideDown 0.5s ease forwards; +} +#matchmakerPopupTitle { + font-size: 1.8em; + color: white; + padding: 0.3em 0.7em; + background: rgba(0,0,0,0.5); + margin-bottom: 0.3em; +} +#matchmakerPopupDescription { + background: rgba(0,0,0,0.5); + color: var(--kpc-yellow); + box-sizing: border-box; + padding: 0.6em 1em; +} +#matchmakerPopupOptions { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + display: flex; +} +.matchmakerPopupButton { + text-align: center; + border: 0.3em solid; + box-sizing: border-box; + margin: 0.5em; + color: white; + border-radius: 0.3em; + font-size: 1.3em; + background-color: rgba(0,0,0,0.5); + padding: 0.2em 1.4em; + transition: all 0.08s; +} +#matchmakerConfirmButton { + border-color: var(--kpc-green); + flex-grow: 1; +} +#matchmakerCancelButton { + border-color: #f44336; +} +.matchmakerPopupButton:hover { + cursor: pointer; + border-color: white !important; + transform: scale(0.95); +} +.matchmakerPopupButton:active { + transform: scale(0.85); +} +`; + +export const TRANSLATOR_CSS = ` +.kpc-translation { + color: #88ff88; + font-style: italic; + margin-left: 8px; + margin-top: 2px; +} +`; + +// ── Alt Manager CSS ── +export const ALT_MANAGER_CSS = ` +.kpc-acc-form { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; } +.kpc-acc-form input { + background: var(--kpc-surface-input); border: 1px solid var(--kpc-border); border-radius: 4px; + color: #fff; padding: 6px 10px; font-size: 13px; outline: none; font-family: inherit; +} +.kpc-acc-form input:focus { border-color: var(--kpc-accent); } +.kpc-acc-form input::placeholder { color: rgba(255,255,255,0.3); } +.kpc-acc-form-buttons { display: flex; gap: 8px; } +.kpc-acc-form-buttons button { + padding: 6px 16px; border: none; border-radius: 4px; cursor: pointer; + font-size: 13px; font-family: inherit; +} +.kpc-acc-form-buttons .kpc-acc-save { + background: var(--kpc-accent); color: #fff; +} +.kpc-acc-form-buttons .kpc-acc-save:hover { filter: brightness(1.2); } +.kpc-acc-form-buttons .kpc-acc-cancel { + background: var(--kpc-surface-hover); color: #fff; +} +.kpc-acc-form-buttons .kpc-acc-cancel:hover { background: var(--kpc-surface-hover-strong); } +.kpc-acc-item { + display: flex; align-items: center; justify-content: space-between; + padding: 8px 12px; background: var(--kpc-surface-card); border-radius: 6px; margin-bottom: 6px; +} +.kpc-acc-item-info { display: flex; align-items: center; gap: 8px; } +.kpc-acc-item-label { color: #fff; font-size: 14px; font-weight: 500; } +.kpc-acc-item-role { + font-size: 11px; padding: 2px 6px; border-radius: 3px; + background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); +} +.kpc-acc-item-actions { display: flex; gap: 6px; } +.kpc-acc-item-actions button { + padding: 4px 12px; border: none; border-radius: 4px; cursor: pointer; + font-size: 12px; font-family: inherit; +} +.kpc-acc-switch { background: var(--kpc-accent); color: #fff; } +.kpc-acc-switch:hover { filter: brightness(1.2); } +.kpc-acc-delete { background: rgba(255,80,80,0.2); color: #ff5050; } +.kpc-acc-delete:hover { background: rgba(255,80,80,0.35); } +.kpc-acc-empty { color: rgba(255,255,255,0.4); font-size: 13px; text-align: center; padding: 16px 0; } +.kpc-alt-overlay-backdrop { + position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 99998; + background: rgba(0,0,0,0.5); +} +.kpc-alt-overlay { + position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); + background: var(--kpc-surface-dialog, #1a1a1a); border-radius: 8px; + padding: 16px; min-width: 280px; max-width: 360px; z-index: 99999; + box-shadow: 0 8px 32px rgba(0,0,0,0.6); +} +.kpc-alt-overlay h3 { + margin: 0 0 12px; color: #fff; font-size: 16px; font-weight: 600; +} +`; + +/** Pre-concatenated CSS for single-call injection (excludes HIDE_ADS_CSS which is separate) */ +export const ALL_CLIENT_CSS = `${CLIENT_SETTINGS_CSS}\n${MATCHMAKER_SETTINGS_CSS}\n${TRANSLATOR_CSS}\n${ALT_MANAGER_CSS}`; diff --git a/src/main/config.ts b/src/main/config.ts new file mode 100644 index 0000000..bf402ce --- /dev/null +++ b/src/main/config.ts @@ -0,0 +1,178 @@ +import Store from 'electron-store'; +import { detectPlatform } from './platform'; + +export interface Keybind { + key: string; + ctrl: boolean; + shift: boolean; + alt: boolean; +} + +export interface SavedAccount { + label: string; + username: string; + password: string; +} + +export interface AppConfig { + window: { + width: number; + height: number; + x: number | undefined; + y: number | undefined; + maximized: boolean; + fullscreen: boolean; + }; + performance: { + fpsUnlocked: boolean; + hardwareAccel: boolean; + gpuPreference: 'high-performance' | 'low-power' | 'default'; + }; + game: { + lastServer: string; + socialTabBehaviour: 'New Window' | 'Same Window'; + joinAsSpectator: boolean; + }; + swapper: { + enabled: boolean; + path: string; + }; + matchmaker: { + enabled: boolean; + regions: string[]; + gamemodes: string[]; + minPlayers: number; + maxPlayers: number; + minRemainingTime: number; + openServerBrowser: boolean; + }; + keybinds: { + reload: Keybind; + newMatch: Keybind; + copyGameLink: Keybind; + joinFromClipboard: Keybind; + devTools: Keybind; + matchmaker: Keybind; + matchmakerAccept: Keybind; + matchmakerCancel: Keybind; + pauseChat: Keybind; + fullscreenToggle: Keybind; + }; + userscripts: { + enabled: boolean; + path: string; + }; + ui: { + showExitButton: boolean; + deathscreenAnimation: boolean; + hideMenuPopups: boolean; + }; + discord: { + enabled: boolean; + }; + translator: { + enabled: boolean; + targetLanguage: string; + showLanguageTag: boolean; + }; + advanced: { + removeUselessFeatures: boolean; + gpuRasterizing: boolean; + helpfulFlags: boolean; + disableAccelerated2D: boolean; + increaseLimits: boolean; + lowLatency: boolean; + experimentalFlags: boolean; + angleBackend: string; + }; + accounts: SavedAccount[]; + platform: { + detectedOS: string; + gpuBackend: string; + }; +} + +export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = { + reload: { key: 'F5', ctrl: false, shift: false, alt: false }, + newMatch: { key: 'F4', ctrl: false, shift: false, alt: false }, + copyGameLink: { key: 'l', ctrl: true, shift: false, alt: false }, + joinFromClipboard: { key: 'j', ctrl: true, shift: false, alt: false }, + devTools: { key: 'F12', ctrl: false, shift: false, alt: false }, + matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false }, + matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false }, + matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false }, + pauseChat: { key: 'F10', ctrl: false, shift: false, alt: false }, + fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false }, +}; + +const platformInfo = detectPlatform(); + +export const config = new Store({ + name: 'krunker-civilian-config', + defaults: { + window: { + width: 1600, + height: 900, + x: undefined, + y: undefined, + maximized: false, + fullscreen: false, + }, + performance: { + fpsUnlocked: true, + hardwareAccel: true, + gpuPreference: 'high-performance', + }, + game: { + lastServer: '', + socialTabBehaviour: 'New Window', + joinAsSpectator: false, + }, + swapper: { + enabled: true, + path: '', + }, + matchmaker: { + enabled: true, + regions: [], + gamemodes: [], + minPlayers: 1, + maxPlayers: 6, + minRemainingTime: 120, + openServerBrowser: true, + }, + keybinds: DEFAULT_KEYBINDS, + userscripts: { + enabled: true, + path: '', + }, + ui: { + showExitButton: true, + deathscreenAnimation: true, + hideMenuPopups: false, + }, + discord: { + enabled: false, + }, + translator: { + enabled: true, + targetLanguage: 'en', + showLanguageTag: true, + }, + advanced: { + removeUselessFeatures: true, + gpuRasterizing: false, + helpfulFlags: true, + disableAccelerated2D: false, + increaseLimits: false, + lowLatency: false, + experimentalFlags: false, + angleBackend: 'default', + }, + accounts: [], + platform: { + detectedOS: platformInfo.os, + gpuBackend: platformInfo.gpuBackend, + }, + }, +}); diff --git a/src/main/discord-rpc.ts b/src/main/discord-rpc.ts new file mode 100644 index 0000000..86ebec1 --- /dev/null +++ b/src/main/discord-rpc.ts @@ -0,0 +1,285 @@ +import { Socket } from 'net'; +import { electronLog } from './logger'; + +const DISCORD_CLIENT_ID = '1474451871694323975'; + +// Discord IPC opcodes +const OP_HANDSHAKE = 0; +const OP_FRAME = 1; +const OP_CLOSE = 2; + +// Rate limit: Discord rejects updates faster than 15s +const RATE_LIMIT_MS = 5000; +const RECONNECT_INTERVAL_MS = 30000; + +export interface ActivityPayload { + details?: string; + state?: string; + startTimestamp?: number; + largeImageKey?: string; + largeImageText?: string; +} + +function getPipePath(id: number): string { + if (process.platform === 'win32') { + return `\\\\?\\pipe\\discord-ipc-${id}`; + } + // Linux/macOS: check XDG_RUNTIME_DIR, TMPDIR, TMP, TEMP, /tmp + const dir = process.env.XDG_RUNTIME_DIR + || process.env.TMPDIR + || process.env.TMP + || process.env.TEMP + || '/tmp'; + return `${dir}/discord-ipc-${id}`; +} + +function encodeFrame(opcode: number, payload: object): Buffer { + const json = JSON.stringify(payload); + const jsonBuf = Buffer.from(json); + const header = Buffer.alloc(8); + header.writeUInt32LE(opcode, 0); + header.writeUInt32LE(jsonBuf.length, 4); + return Buffer.concat([header, jsonBuf]); +} + +export class DiscordRPC { + private socket: Socket | null = null; + private connected = false; + private reconnectTimer: ReturnType | null = null; + private lastUpdate = 0; + private nonce = 0; + private destroyed = false; + private recvBuf = Buffer.alloc(0); + private pendingActivity: ActivityPayload | null = null; + private flushTimer: ReturnType | null = null; + + get isConnected(): boolean { + return this.connected; + } + + connect(): void { + if (this.destroyed) return; + this.tryConnect(0); + } + + private tryConnect(pipeIndex: number): void { + if (this.destroyed || pipeIndex > 9) { + this.scheduleReconnect(); + return; + } + + const pipePath = getPipePath(pipeIndex); + const sock = new Socket(); + let settled = false; + + const onError = () => { + if (settled) return; + settled = true; + sock.destroy(); + // Try next pipe index + this.tryConnect(pipeIndex + 1); + }; + + sock.once('error', onError); + + sock.connect(pipePath, () => { + if (settled || this.destroyed) { + sock.destroy(); + return; + } + settled = true; + this.socket = sock; + this.recvBuf = Buffer.alloc(0); + + // Remove the initial error handler and set up persistent ones + sock.removeListener('error', onError); + sock.on('error', (err) => { + electronLog.warn('[KCC-Discord] Socket error:', err.message); + this.handleDisconnect(); + }); + sock.on('close', () => { + this.handleDisconnect(); + }); + sock.on('data', (data) => { + this.onData(data); + }); + + // Send handshake + const handshake = encodeFrame(OP_HANDSHAKE, { + v: 1, + client_id: DISCORD_CLIENT_ID, + }); + sock.write(handshake); + }); + + // Connection timeout — 5s + sock.setTimeout(5000, onError); + } + + private onData(data: Buffer): void { + this.recvBuf = Buffer.concat([this.recvBuf, data]); + + while (this.recvBuf.length >= 8) { + const opcode = this.recvBuf.readUInt32LE(0); + const length = this.recvBuf.readUInt32LE(4); + + if (this.recvBuf.length < 8 + length) break; + + const jsonBuf = this.recvBuf.slice(8, 8 + length); + this.recvBuf = this.recvBuf.slice(8 + length); + + try { + const payload = JSON.parse(jsonBuf.toString()); + this.handleMessage(opcode, payload); + } catch { + // Malformed JSON — ignore + } + } + } + + private handleMessage(opcode: number, payload: any): void { + if (opcode === OP_FRAME) { + if (payload.cmd === 'DISPATCH' && payload.evt === 'READY') { + this.connected = true; + electronLog.log('[KCC-Discord] Connected to Discord'); + // Flush any activity that was set before connection completed + if (this.pendingActivity) { + this.sendActivity(this.pendingActivity); + this.pendingActivity = null; + } + } + } else if (opcode === OP_CLOSE) { + electronLog.warn('[KCC-Discord] Discord closed connection:', payload.message || ''); + this.handleDisconnect(); + } + } + + private handleDisconnect(): void { + if (!this.connected && !this.socket) return; + this.connected = false; + if (this.flushTimer) { + clearTimeout(this.flushTimer); + this.flushTimer = null; + } + if (this.socket) { + this.socket.destroy(); + this.socket = null; + } + this.recvBuf = Buffer.alloc(0); + electronLog.log('[KCC-Discord] Disconnected'); + this.scheduleReconnect(); + } + + private scheduleReconnect(): void { + if (this.destroyed || this.reconnectTimer) return; + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + if (!this.destroyed && !this.connected) { + this.tryConnect(0); + } + }, RECONNECT_INTERVAL_MS); + } + + setActivity(activity: ActivityPayload): void { + if (this.destroyed) return; + + // Always store latest activity so it can be sent on (re)connect + this.pendingActivity = activity; + + if (!this.connected || !this.socket) return; + + const now = Date.now(); + const elapsed = now - this.lastUpdate; + if (elapsed < RATE_LIMIT_MS) { + // Schedule a flush after the rate limit window expires + if (!this.flushTimer) { + this.flushTimer = setTimeout(() => { + this.flushTimer = null; + if (this.pendingActivity && this.connected && this.socket) { + this.sendActivity(this.pendingActivity); + this.pendingActivity = null; + } + }, RATE_LIMIT_MS - elapsed); + } + return; + } + + this.sendActivity(activity); + this.pendingActivity = null; + } + + private sendActivity(activity: ActivityPayload): void { + if (!this.socket || this.destroyed) return; + this.lastUpdate = Date.now(); + + const activityObj: any = {}; + if (activity.details) activityObj.details = activity.details; + if (activity.state) activityObj.state = activity.state; + if (activity.startTimestamp) { + activityObj.timestamps = { start: activity.startTimestamp }; + } + if (activity.largeImageKey) { + activityObj.assets = { + large_image: activity.largeImageKey, + large_text: activity.largeImageText || 'Krunker Civilian Client', + }; + } + + const frame = encodeFrame(OP_FRAME, { + cmd: 'SET_ACTIVITY', + args: { + pid: process.pid, + activity: activityObj, + }, + nonce: String(++this.nonce), + }); + + try { + this.socket.write(frame); + } catch (err) { + electronLog.warn('[KCC-Discord] Write error:', (err as Error).message); + } + } + + clearActivity(): void { + if (!this.connected || !this.socket || this.destroyed) return; + + const frame = encodeFrame(OP_FRAME, { + cmd: 'SET_ACTIVITY', + args: { + pid: process.pid, + activity: null, + }, + nonce: String(++this.nonce), + }); + + try { + this.socket.write(frame); + } catch { + // Silent + } + } + + disconnect(): void { + this.destroyed = true; + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.flushTimer) { + clearTimeout(this.flushTimer); + this.flushTimer = null; + } + if (this.socket) { + try { + this.clearActivity(); + } catch { + // Silent + } + this.socket.destroy(); + this.socket = null; + } + this.connected = false; + this.recvBuf = Buffer.alloc(0); + } +} diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..cfa1786 --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,670 @@ +import { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, protocol, session, shell } from 'electron'; +import { join } from 'path'; +import { existsSync, mkdirSync, promises as fsp } from 'fs'; +import { get as httpsGet } from 'https'; +import { execFile } from 'child_process'; +import { detectPlatform, applyPlatformFlags } from './platform'; +import { config, Keybind, DEFAULT_KEYBINDS, SavedAccount } from './config'; +import { initSwapperProtocol, registerSwapperFileProtocol, ResourceSwapper } from './swapper'; +import { UserscriptManager } from './userscripts'; +import { ALL_CLIENT_CSS } from './client-ui'; +import { electronLog, getLogPath, closeLogStreams } from './logger'; +import { checkForUpdate, downloadUpdate, installUpdate } from './updater'; +import { showUpdateWindow } from './update-window'; +import { DiscordRPC } from './discord-rpc'; + +// ── App version for API calls ── +const appVersion: string = require('../../package.json').version; + +// ── Region ping cache ── +const SERVER_MAP: Record = { + 'us-ca-sv': 'SV', 'jb-hnd': 'TOK', 'de-fra': 'FRA', + 'as-mb': 'MBI', 'au-syd': 'SYD', 'sgp': 'SIN', + 'us-tx': 'DAL', 'me-bhn': 'BHN', 'brz': 'BRZ', 'us-nj': 'NY', +}; +let pingCache: Record = {}; +let pingCacheTime = 0; + +function osPing(host: string): Promise { + return new Promise((resolve) => { + const isWin = process.platform === 'win32'; + const args = isWin ? ['-n', '1', '-w', '1500', host] : ['-c', '1', '-W', '2', host]; + execFile('ping', args, { timeout: 3000 }, (err, stdout) => { + if (err) { resolve(-1); return; } + const match = stdout.match(/time[=<]([\d.]+)\s*ms/i); + if (match) resolve(Math.round(parseFloat(match[1]))); + else resolve(-1); + }); + }); +} + +// ── Platform flags (must run before app.ready) ── +const platformInfo = detectPlatform(); +const advancedDefaults = { + removeUselessFeatures: true, + gpuRasterizing: false, + helpfulFlags: true, + disableAccelerated2D: false, + increaseLimits: false, + lowLatency: false, + experimentalFlags: false, +}; +const advancedConfig = { ...advancedDefaults, ...config.get('advanced') }; +const perfConfig = { fpsUnlocked: true, ...config.get('performance') }; +applyPlatformFlags(platformInfo, advancedConfig, perfConfig); + +// ── Resource swapper protocol (must register before app.ready) ── +initSwapperProtocol(); + +// ── Ad-blocking URL patterns (matched in C++ layer, never hits JS for non-matches) ── +const BLOCKED_URL_PATTERNS = [ + '*://*.pollfish.com/*', + '*://www.paypalobjects.com/*', + '*://fran-cdn.frvr.com/*', + '*://c.amazon-adsystem.com/*', + '*://cdn.frvr.com/fran/*', + '*://cookiepro.com/*', + '*://*.cookiepro.com/*', + '*://www.googletagmanager.com/*', + '*://*.doubleclick.net/*', + '*://storage.googleapis.com/pollfish_production/*', + '*://coeus.frvr.com/*', + '*://apis.google.com/js/platform.js', + '*://imasdk.googleapis.com/*', +]; + +// ── CSS to hide ad containers ── +const HIDE_ADS_CSS = ` +.endAHolder, +#aHider, +#adCon, +#rightABox, +#aContainer, +#topRightAdHolder, +div#aContainer, +#braveWarning, +#topRightAdHolder { + display: none !important; +}`; + +// ── Consent dismiss script (polling only — NO MutationObserver on main frame) ── +const CONSENT_DISMISS_MAIN_JS = ` +(function dismissConsent() { + let attempts = 0; + const timer = setInterval(() => { + attempts++; + const btn = document.querySelector('.fc-cta-consent, [aria-label="Consent"], .css-47sehv'); + if (btn) { btn.click(); clearInterval(timer); } + if (attempts > 30) clearInterval(timer); + }, 500); +})();`; + +// ── Escape pointer lock fix ── +const ESCAPE_POINTERLOCK_FIX_JS = ` +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && document.pointerLockElement) { + document.exitPointerLock(); + } +}, true);`; + +// ── Keybind matching ── +function matchesKeybind(input: { key: string; control: boolean; shift: boolean; alt: boolean }, bind: Keybind | undefined): boolean { + if (!bind) return false; + return input.key === bind.key + && input.control === bind.ctrl + && input.shift === bind.shift + && input.alt === bind.alt; +} + +// ── Cached keybinds (avoid re-reading electron-store on every keypress) ── +let cachedKeybinds: Record | null = null; + +function getKeybinds(): Record { + if (!cachedKeybinds) { + cachedKeybinds = { ...DEFAULT_KEYBINDS, ...config.get('keybinds') }; + } + return cachedKeybinds; +} + +// ── Debounced window state persistence ── +let saveTimer: ReturnType | null = null; + +function saveWindowState(win: BrowserWindow): void { + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(() => { + if (win.isDestroyed()) return; + const bounds = win.getBounds(); + config.set('window', { + width: bounds.width, + height: bounds.height, + x: bounds.x, + y: bounds.y, + maximized: win.isMaximized(), + fullscreen: win.isFullScreen(), + }); + }, 1000); +} + +app.whenReady().then(async () => { + electronLog.log('[KCC] App ready'); + + // ── Auto-update check (mandatory, Windows NSIS install only) ── + const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR; + const isAppImage = !!process.env.APPIMAGE; + const isDev = !app.isPackaged; + if (isDev || process.platform !== 'win32' || isPortable || isAppImage) { + electronLog.log('[KCC] Skipping auto-update (portable or non-Windows)'); + } else { + try { + electronLog.log('[KCC] Checking for updates...'); + const update = await checkForUpdate(appVersion); + if (update) { + electronLog.log(`[KCC] Update available: v${update.version}`); + const { window: updateWin, sendProgress } = showUpdateWindow(); + sendProgress(`Update available (v${update.version})`, 0); + + const tempDir = join(app.getPath('temp'), 'kcc-update'); + if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true }); + const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`); + + let cancelled = false; + updateWin.on('closed', () => { cancelled = true; }); + + try { + await downloadUpdate(update.downloadUrl, installerPath, (pct) => { + if (!cancelled && !updateWin.isDestroyed()) { + sendProgress(`Downloading update... ${pct}%`, pct); + } + }); + + if (!cancelled) { + sendProgress('Installing update...', 100); + installUpdate(installerPath); + return; // app.quit() called by installUpdate + } + } catch (err) { + electronLog.error('[KCC] Update download failed:', err); + if (!updateWin.isDestroyed()) updateWin.close(); + } + } else { + electronLog.log('[KCC] No updates available'); + } + } catch (err) { + electronLog.error('[KCC] Update check failed:', err); + } + } + + await launchApp(); +}); + +async function launchApp(): Promise { + electronLog.log('[KCC] Starting initialization'); + + // ── Register swapper file protocol ── + registerSwapperFileProtocol(); + + // ── Session: persistent partition + clean user-agent ── + const ses = session.fromPartition('persist:krunker'); + const rawUA = ses.getUserAgent(); + ses.setUserAgent(rawUA.replace(/\s*krunker-civilian-client\/\S+/i, '')); + + // ── Resource swapper ── + const swapperConfig = config.get('swapper'); + const swapDir = swapperConfig.path || join(app.getPath('userData'), 'KCCClient', 'swapper'); + const swapper = swapperConfig.enabled ? new ResourceSwapper(swapDir) : null; + electronLog.log(`[KCC] Resource swapper: ${swapper ? 'enabled' : 'disabled'} (${swapDir})`); + + // ── Userscript manager ── + const usConfig = config.get('userscripts') || { enabled: true, path: '' }; + const usDir = usConfig.path || join(app.getPath('userData'), 'KCCClient'); + const userscriptManager = usConfig.enabled ? new UserscriptManager(usDir) : null; + electronLog.log(`[KCC] Userscripts: ${userscriptManager ? 'enabled' : 'disabled'} (${usDir})`); + + // ── Ad blocking + resource swapper (single onBeforeRequest — Electron only allows one) ── + ses.webRequest.onBeforeRequest({ urls: [...BLOCKED_URL_PATTERNS] }, (details, callback) => { + if (swapper) { + const redirect = swapper.getRedirect(details.url); + if (redirect) return callback({ redirectURL: redirect }); + } + callback({ cancel: true }); + }); + + // Once swapper scan finishes, re-register with swapper patterns included + if (swapper) { + swapper.waitForReady().then(() => { + const filterUrls = [...BLOCKED_URL_PATTERNS, ...swapper.patterns]; + ses.webRequest.onBeforeRequest({ urls: filterUrls }, (details, callback) => { + const redirect = swapper.getRedirect(details.url); + if (redirect) return callback({ redirectURL: redirect }); + callback({ cancel: true }); + }); + electronLog.log(`[KCC] Swapper ready: ${swapper.patterns.length} pattern(s)`); + }); + } + + // ── CORS fix for swapped resources ── + if (swapper) { + ses.webRequest.onHeadersReceived(({ responseHeaders }, callback) => { + if (!responseHeaders) return callback({}); + for (const key in responseHeaders) { + const lowercase = key.toLowerCase(); + if (lowercase === 'access-control-allow-credentials' && responseHeaders[key][0] === 'true') { + return callback({ responseHeaders }); + } + if (lowercase === 'access-control-allow-origin') { + delete responseHeaders[key]; + break; + } + } + return callback({ + responseHeaders: { ...responseHeaders, 'access-control-allow-origin': ['*'] }, + }); + }); + } + + // ── Restore saved window bounds ── + const savedWindow = config.get('window'); + + const win = new BrowserWindow({ + width: savedWindow.width, + height: savedWindow.height, + x: savedWindow.x, + y: savedWindow.y, + frame: true, + backgroundColor: '#000000', + webPreferences: { + preload: join(__dirname, '..', 'preload', 'index.js'), + session: ses, + contextIsolation: false, + nodeIntegration: false, + sandbox: false, + spellcheck: false, + backgroundThrottling: false, + }, + }); + + if (savedWindow.fullscreen) win.setFullScreen(true); + else if (savedWindow.maximized) win.maximize(); + + // ── No application menu (prevents Escape/Alt interception) ── + Menu.setApplicationMenu(null); + + // ── Discord Rich Presence ── + let discordRpc: DiscordRPC | null = null; + { + const discordConf = config.get('discord') || { enabled: false }; + if (discordConf.enabled) { + discordRpc = new DiscordRPC(); + discordRpc.connect(); + electronLog.log('[KCC] Discord Rich Presence enabled'); + } + } + + // ── Common output directory (used by folder actions) ── + const outputDir = join(app.getPath('documents'), 'KrunkerCivilianClient'); + + // ── Configurable keybinds via before-input-event ── + win.webContents.on('before-input-event', (event, input) => { + if (input.type !== 'keyDown') return; + + const binds = getKeybinds(); + + if (matchesKeybind(input, binds.reload)) { + win.reload(); + event.preventDefault(); + } else if (matchesKeybind(input, binds.newMatch)) { + win.loadURL('https://krunker.io'); + event.preventDefault(); + } else if (matchesKeybind(input, binds.joinFromClipboard)) { + const text = clipboard.readText(); + try { const u = new URL(text); if (u.protocol === 'https:' && u.hostname.endsWith('krunker.io')) win.loadURL(text); } catch {}; + event.preventDefault(); + } else if (matchesKeybind(input, binds.copyGameLink)) { + clipboard.writeText(win.webContents.getURL()); + event.preventDefault(); + } else if (matchesKeybind(input, binds.devTools)) { + win.webContents.toggleDevTools(); + event.preventDefault(); + } else if (matchesKeybind(input, binds.matchmaker)) { + const mm = config.get('matchmaker'); + if (mm.enabled) { + win.webContents.send('matchmaker-find', { + ...mm, + acceptKey: binds.matchmakerAccept, + cancelKey: binds.matchmakerCancel, + }); + } else { + win.loadURL('https://krunker.io'); + } + event.preventDefault(); + } else if (matchesKeybind(input, binds.pauseChat)) { + win.webContents.send('toggle-chat-pause'); + event.preventDefault(); + } else if (matchesKeybind(input, binds.fullscreenToggle)) { + win.setFullScreen(!win.isFullScreen()); + event.preventDefault(); + } + }); + + // ── Window state persistence (debounced) ── + win.on('resize', () => saveWindowState(win)); + win.on('move', () => saveWindowState(win)); + win.on('maximize', () => saveWindowState(win)); + win.on('unmaximize', () => saveWindowState(win)); + win.on('enter-full-screen', () => saveWindowState(win)); + win.on('leave-full-screen', () => saveWindowState(win)); + + // ── Open krunker.io sub-pages in a new window ── + const GAME_PAGE_PATHS = ['/', '']; + function isGameURL(url: string): boolean { + try { + const parsed = new URL(url); + if (!parsed.hostname.includes('krunker.io')) return false; + return GAME_PAGE_PATHS.includes(parsed.pathname); + } catch { return false; } + } + + function openSubWindow(url: string): void { + const sub = new BrowserWindow({ + width: 1280, + height: 720, + frame: true, + backgroundColor: '#000000', + autoHideMenuBar: true, + webPreferences: { + preload: join(__dirname, '..', 'preload', 'index.js'), + session: ses, + contextIsolation: false, + nodeIntegration: false, + sandbox: false, + spellcheck: false, + }, + }); + sub.removeMenu(); + sub.loadURL(url); + sub.webContents.on('did-finish-load', () => { + sub.webContents.insertCSS(ALL_CLIENT_CSS).catch(() => {}); + sub.webContents.send('main_did-finish-load'); + }); + sub.webContents.setWindowOpenHandler(({ url: subUrl }) => { + if (subUrl.includes('krunker.io')) { + sub.loadURL(subUrl); + } else { + setImmediate(() => shell.openExternal(subUrl)); + } + return { action: 'deny' }; + }); + sub.webContents.on('will-prevent-unload', (ev) => { + const choice = dialog.showMessageBoxSync(sub, { + type: 'question', + buttons: ['Leave', 'Stay'], + defaultId: 1, + title: 'Leave page?', + message: 'Changes you made may not be saved.', + }); + if (choice === 0) ev.preventDefault(); + }); + } + + // ── Cached game config (invalidated on set-config writes to 'game') ── + const gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window' }; + let cachedGameConf: typeof gameDefaults | null = null; + function getGameConf(): typeof gameDefaults { + if (!cachedGameConf) cachedGameConf = { ...gameDefaults, ...config.get('game') }; + return cachedGameConf; + } + + // Intercept in-page navigation (e.g. window.location = '/social.html') + win.webContents.on('will-navigate', (event, url) => { + if (url.includes('krunker.io') && !isGameURL(url)) { + if (getGameConf().socialTabBehaviour === 'New Window') { + event.preventDefault(); + openSubWindow(url); + } + } + }); + + // Intercept target="_blank" / window.open links + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.includes('krunker.io')) { + if (isGameURL(url)) { + win.loadURL(url); + } else { + if (getGameConf().socialTabBehaviour === 'New Window') { + openSubWindow(url); + } else { + win.loadURL(url); + } + } + } else { + setImmediate(() => shell.openExternal(url)); + } + return { action: 'deny' }; + }); + + // ── Inject scripts after page loads ── + win.webContents.on('did-finish-load', () => { + electronLog.log(`[KCC] Page loaded: ${win.webContents.getURL()}`); + Promise.all([ + win.webContents.insertCSS(HIDE_ADS_CSS), + win.webContents.insertCSS(ALL_CLIENT_CSS), + ]).catch(() => {}); + + win.webContents.executeJavaScript(ESCAPE_POINTERLOCK_FIX_JS).catch((err) => electronLog.warn('[KCC] Pointerlock fix inject failed:', err)); + win.webContents.executeJavaScript(CONSENT_DISMISS_MAIN_JS).catch((err) => electronLog.warn('[KCC] Consent dismiss inject failed:', err)); + // Notify preload to start hooking settings (matches Crankshaft's timing) + win.webContents.send('main_did-finish-load'); + }); + + // ── IPC handlers ── + ipcMain.handle('get-version', () => appVersion); + ipcMain.handle('get-platform', () => platformInfo); + ipcMain.handle('get-config', (_e, key: string) => config.get(key as keyof typeof config.store)); + ipcMain.handle('get-all-config', (_e, keys: string[]) => { + const result: Record = {}; + for (const key of keys) result[key] = config.get(key as keyof typeof config.store); + return result; + }); + let configWriteTimer: ReturnType | null = null; + const pendingConfigWrites = new Map(); + + ipcMain.handle('set-config', (_e, key: string, value: unknown) => { + // Flush immediately for keys that have side effects + if (key === 'keybinds') { + config.set(key as any, value); + cachedKeybinds = null; + return; + } + pendingConfigWrites.set(key, value); + if (!configWriteTimer) { + configWriteTimer = setTimeout(() => { + for (const [k, v] of pendingConfigWrites) { + config.set(k as any, v); + } + // Invalidate caches for keys that affect runtime behavior + if (pendingConfigWrites.has('game')) cachedGameConf = null; + pendingConfigWrites.clear(); + configWriteTimer = null; + }, 300); + } + }); + ipcMain.handle('window-minimize', () => win.minimize()); + ipcMain.handle('window-maximize', () => { + if (win.isMaximized()) win.unmaximize(); else win.maximize(); + }); + ipcMain.handle('window-close', () => win.close()); + ipcMain.handle('window-is-maximized', () => win.isMaximized()); + ipcMain.handle('toggle-devtools', () => win.webContents.toggleDevTools()); + ipcMain.handle('inject-game-click', () => { + const [width, height] = win.getContentSize(); + const x = Math.round(width / 2); + const y = Math.round(height / 2); + win.webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount: 1 }); + win.webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount: 1 }); + }); + ipcMain.handle('get-swap-dir', () => swapDir); + ipcMain.handle('open-swap-folder', () => shell.openPath(swapDir)); + + // ── Ping regions IPC handler (TCP connect timing, cached 60s) ── + ipcMain.handle('ping-regions', async () => { + if (Object.keys(pingCache).length > 0 && Date.now() - pingCacheTime < 60000) { + return pingCache; + } + try { + const data = await new Promise((resolve, reject) => { + httpsGet('https://matchmaker.krunker.io/ping-list?hostname=krunker.io', { rejectUnauthorized: false }, (res) => { + let body = ''; + res.on('data', (chunk: string) => { body += chunk; }); + res.on('end', () => resolve(body)); + res.on('error', reject); + }).on('error', reject); + }); + const serverIPs: Record = JSON.parse(data); + + const results: Record = {}; + + async function pingWithRetry(host: string): Promise { + const latency = await osPing(host); + if (latency >= 0) return latency; + const retry = await osPing(host); + return retry >= 0 ? retry : -1; + } + + const promises = Object.entries(serverIPs).map(async ([server, ip]) => { + const regionName = SERVER_MAP[server] ?? server; + const host = ip.split(':')[0]; + const latency = await pingWithRetry(host); + if (latency >= 0) { + results[regionName] = latency; + } + }); + await Promise.allSettled(promises); + pingCache = results; + pingCacheTime = Date.now(); + + return results; + } catch (err) { + electronLog.error('[KCC] Ping regions error:', err); + return pingCache; + } + }); + + // ── Discord Rich Presence IPC handler ── + ipcMain.on('discord-update', (_e, activity: any) => { + discordRpc?.setActivity(activity); + }); + + // ── Verbose log IPC handler (preload forwards logs here) ── + ipcMain.on('verbose-log', (_e, level: string, ...args: unknown[]) => { + if (level === 'error') electronLog.error(...args); + else if (level === 'warn') electronLog.warn(...args); + else electronLog.log(...args); + }); + + // ── Userscript IPC handlers ── + ipcMain.handle('userscripts-get-dir', () => userscriptManager ? userscriptManager.dir : ''); + ipcMain.handle('userscripts-open-folder', () => { + if (userscriptManager) shell.openPath(userscriptManager.dir); + }); + ipcMain.handle('userscripts-scan', async () => { + if (!userscriptManager) return { scripts: [], tracker: {} }; + const scripts = await userscriptManager.scanScripts(); + const tracker = await userscriptManager.loadTracker(scripts); + return { scripts, tracker }; + }); + ipcMain.handle('userscripts-set-tracker', (_e, tracker: Record) => { + if (userscriptManager) userscriptManager.saveTracker(tracker); + }); + ipcMain.handle('userscripts-load-prefs', (_e, filename: string) => { + if (!userscriptManager) return {}; + return userscriptManager.loadScriptPrefs(filename); + }); + ipcMain.handle('userscripts-save-prefs', (_e, filename: string, prefs: Record) => { + if (userscriptManager) userscriptManager.saveScriptPrefs(filename, prefs); + }); + + // ── Action button IPC handlers ── + ipcMain.handle('open-electron-log', () => { + shell.openPath(getLogPath('electron')); + }); + ipcMain.handle('reset-swapper', async () => { + try { + const entries = await fsp.readdir(swapDir, { withFileTypes: true }); + for (const entry of entries) { + await fsp.rm(join(swapDir, entry.name), { recursive: true, force: true }); + } + return true; + } catch (err) { + electronLog.error('[KCC] Reset swapper failed:', err); + return false; + } + }); + ipcMain.handle('restart-client', () => { + app.relaunch(); + app.quit(); + }); + ipcMain.handle('reset-options', () => { + config.clear(); + app.relaunch(); + app.quit(); + }); + ipcMain.handle('delete-all-data', async () => { + config.clear(); + const userData = app.getPath('userData'); + try { + await fsp.rm(join(userData, 'logs'), { recursive: true, force: true }); + } catch (err) { + electronLog.warn('[KCC] Partial data deletion failed (non-fatal):', err); + } + app.relaunch(); + app.quit(); + }); + + // ── Alt manager IPC handlers ── + ipcMain.handle('alt-list', () => config.get('accounts') || []); + + ipcMain.handle('alt-save', (_e, account: SavedAccount) => { + const accounts = config.get('accounts') || []; + accounts.push(account); + config.set('accounts', accounts); + return { success: true, index: accounts.length - 1 }; + }); + + ipcMain.handle('alt-remove', (_e, index: number) => { + const accounts = config.get('accounts') || []; + if (index < 0 || index >= accounts.length) return { success: false }; + accounts.splice(index, 1); + config.set('accounts', accounts); + return { success: true }; + }); + + ipcMain.handle('alt-rename', (_e, index: number, newLabel: string) => { + const accounts = config.get('accounts') || []; + if (index < 0 || index >= accounts.length) return { success: false }; + accounts[index].label = newLabel; + config.set('accounts', accounts); + return { success: true }; + }); + + // ── Stop page immediately on close to kill audio ── + win.on('close', () => { + win.webContents.setAudioMuted(true); + win.webContents.stop(); + }); + + // ── Shutdown: disconnect Discord, then close log streams ── + app.on('will-quit', () => { + discordRpc?.disconnect(); + electronLog.log('[KCC] Shutting down'); + closeLogStreams(); + }); + + electronLog.log('[KCC] Initialization complete — loading game'); + + // ── Load the game ── + win.loadURL('https://krunker.io'); +} + +app.on('window-all-closed', () => { + app.quit(); +}); diff --git a/src/main/logger.ts b/src/main/logger.ts new file mode 100644 index 0000000..447a533 --- /dev/null +++ b/src/main/logger.ts @@ -0,0 +1,80 @@ +import { app } from 'electron'; +import { join } from 'path'; +import { existsSync, mkdirSync, readdirSync, unlinkSync, createWriteStream, WriteStream } from 'fs'; + +const LOG_RETENTION_DAYS = 7; + +let electronStream: WriteStream; +let electronPath: string; +let ready = false; + +function dateStamp(): string { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +function pruneOldLogs(logDir: string): void { + try { + const cutoff = Date.now() - LOG_RETENTION_DAYS * 86400000; + for (const file of readdirSync(logDir)) { + const m = file.match(/^electron-(\d{4}-\d{2}-\d{2})\.log$/); + if (!m) continue; + const fileDate = new Date(m[1] + 'T00:00:00').getTime(); + if (fileDate < cutoff) { + try { unlinkSync(join(logDir, file)); } catch { /* ignore */ } + } + } + } catch { /* ignore */ } +} + +function init(): void { + if (ready) return; + const logDir = join(app.getPath('userData'), 'logs'); + if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true }); + + pruneOldLogs(logDir); + + const stamp = dateStamp(); + electronPath = join(logDir, `electron-${stamp}.log`); + + // Append to today's log — one file per day, multiple sessions + electronStream = createWriteStream(electronPath, { flags: 'a' }); + + const sep = `\n${'='.repeat(60)}\n Session started ${new Date().toISOString()}\n${'='.repeat(60)}\n`; + electronStream.write(sep); + ready = true; +} + +function ts(): string { + return new Date().toISOString(); +} + +function fmt(...args: unknown[]): string { + return args.map(a => { + if (a instanceof Error) return `${a.message}\n${a.stack}`; + if (typeof a === 'string') return a; + try { return JSON.stringify(a); } catch { return String(a); } + }).join(' '); +} + +function makeLogger(getStream: () => WriteStream) { + return { + log: (...args: unknown[]) => { init(); const m = fmt(...args); console.log(m); if (!closed) getStream().write(`[${ts()}] ${m}\n`); }, + warn: (...args: unknown[]) => { init(); const m = fmt(...args); console.warn(m); if (!closed) getStream().write(`[${ts()}] WARN: ${m}\n`); }, + error: (...args: unknown[]) => { init(); const m = fmt(...args); console.error(m); if (!closed) getStream().write(`[${ts()}] ERROR: ${m}\n`); }, + }; +} + +export const electronLog = makeLogger(() => electronStream); + +export function getLogPath(type: 'electron'): string { + init(); + return electronPath; +} + +let closed = false; + +export function closeLogStreams(): void { + closed = true; + if (electronStream) electronStream.end(); +} diff --git a/src/main/platform.ts b/src/main/platform.ts new file mode 100644 index 0000000..1086532 --- /dev/null +++ b/src/main/platform.ts @@ -0,0 +1,128 @@ +import { app } from 'electron'; +import type { AppConfig } from './config'; + +export type Platform = 'win32' | 'linux' | 'darwin'; +export type GpuBackend = 'angle' | 'opengl' | 'vulkan' | 'default'; + +export interface PlatformInfo { + os: Platform; + isWindows: boolean; + isLinux: boolean; + useNativeTitlebar: boolean; + gpuBackend: GpuBackend; +} + +export function detectPlatform(): PlatformInfo { + const os = process.platform as Platform; + const isWindows = os === 'win32'; + const isLinux = os === 'linux'; + + return { + os, + isWindows, + isLinux, + useNativeTitlebar: isLinux, + gpuBackend: isWindows ? 'angle' : 'default', + }; +} + +export function applyPlatformFlags(info: PlatformInfo, advanced: AppConfig['advanced'], performance: AppConfig['performance']): void { + // ── FPS uncap ── + // disable-frame-rate-limit causes compositor CPU spin on Chromium 84+, starving + // input events. On Electron 42 (Chromium 147), this is fixed by a patch to + // cc/scheduler/scheduler.cc in our custom Electron build. The latency recovery + // flags below are no-ops on Chromium 94+ (features were removed), but are + // harmless to keep — Chromium ignores unknown feature flags. + if (performance.fpsUnlocked) { + app.commandLine.appendSwitch('disable-frame-rate-limit'); + app.commandLine.appendSwitch('disable-gpu-vsync'); + app.commandLine.appendSwitch('max-gum-fps', '9999'); + app.commandLine.appendSwitch('enable-features', 'ImplLatencyRecovery,MainLatencyRecovery'); + } + + // ── Always-on platform flags ── + app.commandLine.appendSwitch('disable-backgrounding-occluded-windows'); + // WebGL is mandatory for Krunker — force it past any GPU blocklist. + // On Chromium 134+ the blocklist is stricter and silently disables WebGL on many Linux GPUs. + app.commandLine.appendSwitch('ignore-gpu-blocklist'); + + // ── ANGLE backend ── + // 'default' means platform default: D3D11 on Windows, no override on Linux + if (advanced.angleBackend && advanced.angleBackend !== 'default') { + app.commandLine.appendSwitch('use-angle', advanced.angleBackend); + } else if (info.isWindows) { + app.commandLine.appendSwitch('use-angle', 'd3d11'); + } + + if (info.isWindows) { + app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion,HardwareMediaKeyHandling'); + } + + if (info.isLinux) { + app.commandLine.appendSwitch('ozone-platform-hint', 'auto'); + // GPU sandbox can fail inside AppImage FUSE mounts and on certain Mesa driver versions, + // causing the GPU process to crash and leaving a black screen. + app.commandLine.appendSwitch('disable-gpu-sandbox'); + } + + // ── Remove useless features ── + if (advanced.removeUselessFeatures) { + app.commandLine.appendSwitch('disable-breakpad'); + app.commandLine.appendSwitch('disable-print-preview'); + app.commandLine.appendSwitch('disable-metrics-reporting'); + app.commandLine.appendSwitch('disable-metrics'); + app.commandLine.appendSwitch('disable-2d-canvas-clip-aa'); + app.commandLine.appendSwitch('disable-logging'); + app.commandLine.appendSwitch('disable-hang-monitor'); + app.commandLine.appendSwitch('disable-component-update'); + } + + // ── GPU rasterization ── + // OOP rasterization is always-on when GPU rasterization is enabled (Chromium 100+) + if (advanced.gpuRasterizing) { + app.commandLine.appendSwitch('enable-gpu-rasterization'); + app.commandLine.appendSwitch('disable-zero-copy'); + } + + // ── Helpful flags ── + if (advanced.helpfulFlags) { + app.commandLine.appendSwitch('enable-javascript-harmony'); + app.commandLine.appendSwitch('enable-future-v8-vm-features'); + app.commandLine.appendSwitch('enable-webgl'); + app.commandLine.appendSwitch('disable-background-timer-throttling'); + app.commandLine.appendSwitch('disable-renderer-backgrounding'); + app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); + } + + // ── Disable accelerated 2D canvas ── + if (advanced.disableAccelerated2D) { + app.commandLine.appendSwitch('disable-accelerated-2d-canvas'); + } + + // ── Increase limits ── + if (advanced.increaseLimits) { + app.commandLine.appendSwitch('renderer-process-limit', '100'); + app.commandLine.appendSwitch('max-active-webgl-contexts', '100'); + app.commandLine.appendSwitch('webrtc-max-cpu-consumption-percentage', '100'); + app.commandLine.appendSwitch('ignore-gpu-blocklist'); + } + + // ── Low latency ── + // High-res timers and QUIC are default on Chromium 100+. Accelerated 2D canvas + // is default on Chromium 42+. These enable flags were removed from the source. + if (advanced.lowLatency) { + app.commandLine.appendSwitch('force-high-performance-gpu'); + } + + // ── Experimental flags ── + // Removed dead flags: enable-accelerated-video-decode (default since Chromium 132), + // enable-native-gpu-memory-buffers (Linux-only), high-dpi-support (removed in ~M54, + // HiDPI is default since M108). Renamed ignore-gpu-blacklist → ignore-gpu-blocklist. + if (advanced.experimentalFlags) { + app.commandLine.appendSwitch('disable-low-end-device-mode'); + app.commandLine.appendSwitch('ignore-gpu-blocklist'); + app.commandLine.appendSwitch('no-pings'); + app.commandLine.appendSwitch('no-proxy-server'); + } +} + diff --git a/src/main/swapper.ts b/src/main/swapper.ts new file mode 100644 index 0000000..c5848f5 --- /dev/null +++ b/src/main/swapper.ts @@ -0,0 +1,95 @@ +import { existsSync, mkdirSync, promises as fsp } from 'fs'; +import { join } from 'path'; +import { protocol, net } from 'electron'; + +const PROTOCOL_NAME = 'kpc-swap'; +const TARGET_DOMAIN = 'krunker.io'; +const STANDARD_ASSET_RE = /^\/(?:models|textures|sound|scares|videos)(?:$|\/)/u; + +/** + * Register the custom protocol scheme. Must be called BEFORE app.ready. + */ +export function initSwapperProtocol(): void { + protocol.registerSchemesAsPrivileged([{ + scheme: PROTOCOL_NAME, + privileges: { secure: true, corsEnabled: true, bypassCSP: true }, + }]); +} + +/** + * Register the file protocol handler. Must be called AFTER app.ready. + */ +export function registerSwapperFileProtocol(): void { + protocol.handle(PROTOCOL_NAME, (request) => { + const filePath = decodeURI(request.url.replace(`${PROTOCOL_NAME}:`, '')); + return net.fetch('file://' + filePath); + }); +} + +/** + * Scans a local directory and intercepts matching Krunker asset requests, + * redirecting them to local replacement files via a custom protocol. + */ +export class ResourceSwapper { + private swapDir: string; + private swapFiles = new Map(); + private ready = false; + private scanPromise: Promise; + + constructor(swapDir: string) { + this.swapDir = swapDir; + if (!existsSync(this.swapDir)) mkdirSync(this.swapDir, { recursive: true }); + this.scanPromise = this.scanAsync(''); + } + + /** Wait for the async directory scan to complete */ + async waitForReady(): Promise { + await this.scanPromise; + this.ready = true; + } + + /** URL filter patterns for webRequest.onBeforeRequest — single broad pattern */ + get patterns(): string[] { + return this.swapFiles.size > 0 ? [`*://*.${TARGET_DOMAIN}/*`] : []; + } + + /** + * Returns a redirect URL if the request should be swapped, null otherwise. + * Strips /assets/ prefix so both `assets.krunker.io/assets/textures/foo.png` + * and `assets.krunker.io/textures/foo.png` resolve to the same local file. + */ + getRedirect(url: string): string | null { + if (!this.ready) return null; + try { + // Extract pathname from URL using string ops (faster than new URL()) + // URLs are like: https://assets.krunker.io/path/file.ext?v=hash + const protoEnd = url.indexOf('//'); + if (protoEnd === -1) return null; + const pathStart = url.indexOf('/', protoEnd + 2); + if (pathStart === -1) return null; + const queryStart = url.indexOf('?', pathStart); + let pathname = queryStart === -1 ? url.substring(pathStart) : url.substring(pathStart, queryStart); + if (pathname.startsWith('/assets/')) pathname = pathname.substring(7); + const localPath = this.swapFiles.get(pathname); + if (localPath) return `${PROTOCOL_NAME}:/${localPath}`; + } catch { /* malformed URL — ignore */ } + return null; + } + + /** Recursively scan the swap directory and build the file map (async) */ + private async scanAsync(prefix: string): Promise { + try { + const entries = await fsp.readdir(join(this.swapDir, prefix), { withFileTypes: true }); + for (const dirent of entries) { + const name = `${prefix}/${dirent.name}`; + if (dirent.isDirectory()) { + await this.scanAsync(name); + } else { + this.swapFiles.set(name, join(this.swapDir, name)); + } + } + } catch (err) { + console.error(`Failed to scan swap directory prefix: ${prefix}`); + } + } +} diff --git a/src/main/update-window.ts b/src/main/update-window.ts new file mode 100644 index 0000000..ae1cdf8 --- /dev/null +++ b/src/main/update-window.ts @@ -0,0 +1,96 @@ +import { BrowserWindow } from 'electron'; + +const UPDATE_HTML = ` + + + + + + +

Krunker Civilian Client

+
Checking for updates...
+
+
+
+ + +`; + +const UPDATE_DATA_URL = 'data:text/html;charset=utf-8,' + encodeURIComponent(UPDATE_HTML); + +export function showUpdateWindow(): { window: BrowserWindow; sendProgress: (message: string, percent?: number) => void } { + const win = new BrowserWindow({ + width: 450, + height: 180, + resizable: false, + alwaysOnTop: true, + backgroundColor: '#1a1a2e', + autoHideMenuBar: true, + title: 'Krunker Civilian Client - Update', + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + sandbox: false, + }, + }); + win.removeMenu(); + + win.loadURL(UPDATE_DATA_URL); + + function sendProgress(message: string, percent?: number): void { + if (!win.isDestroyed()) { + win.webContents.send('update-progress', message, percent); + } + } + + return { window: win, sendProgress }; +} diff --git a/src/main/updater.ts b/src/main/updater.ts new file mode 100644 index 0000000..ca4990c --- /dev/null +++ b/src/main/updater.ts @@ -0,0 +1,212 @@ +import { get as httpsGet, request as httpsRequest } from 'https'; +import { createWriteStream, renameSync, unlinkSync, existsSync } from 'fs'; +import { spawn } from 'child_process'; +import { app } from 'electron'; +import { electronLog } from './logger'; + +export interface UpdateInfo { + version: string; + downloadUrl: string; + fileSize: number; +} + +export type ProgressCallback = (percent: number) => void; + +const UPDATE_CONFIG = { + // Gitea provider (swap these for kpdclient.com migration) + checkUrl: 'https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client/releases/latest', + assetPattern: /Setup\.exe$/i, + rejectUnauthorized: false, +}; + +const CHECK_TIMEOUT_MS = 10000; +const DOWNLOAD_TIMEOUT_MS = 300000; // 5 minutes + +/** + * Simple semver comparison: returns true if a < b. + * Handles versions like "0.1.0", "1.2.3". + */ +function versionLessThan(a: string, b: string): boolean { + const pa = a.split('.').map(Number); + const pb = b.split('.').map(Number); + const len = Math.max(pa.length, pb.length); + for (let i = 0; i < len; i++) { + const na = pa[i] || 0; + const nb = pb[i] || 0; + if (na < nb) return true; + if (na > nb) return false; + } + return false; +} + +export function checkForUpdate(currentVersion: string): Promise { + return new Promise((resolve) => { + electronLog.log('[KCC-Update] Checking for updates at:', UPDATE_CONFIG.checkUrl); + electronLog.log('[KCC-Update] Current version:', currentVersion); + + const req = httpsGet(UPDATE_CONFIG.checkUrl, { + rejectUnauthorized: UPDATE_CONFIG.rejectUnauthorized, + headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion }, + }, (res) => { + electronLog.log('[KCC-Update] Check response status:', res.statusCode); + // Follow redirects + if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + electronLog.log('[KCC-Update] Redirected to:', res.headers.location); + httpsGet(res.headers.location, { + rejectUnauthorized: UPDATE_CONFIG.rejectUnauthorized, + headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion }, + }, (redirectRes) => { + electronLog.log('[KCC-Update] Redirect response status:', redirectRes.statusCode); + handleResponse(redirectRes); + }).on('error', (err) => { + electronLog.error('[KCC-Update] Redirect error:', err); + resolve(null); + }); + return; + } + handleResponse(res); + }); + + function handleResponse(res: import('http').IncomingMessage): void { + if (res.statusCode !== 200) { + electronLog.error('[KCC-Update] Check returned status', res.statusCode); + resolve(null); + return; + } + + let data = ''; + res.on('data', (chunk: string) => { data += chunk; }); + res.on('end', () => { + try { + const release = JSON.parse(data); + const tagName: string = release.tag_name || ''; + const remoteVersion = tagName.replace(/^v/i, ''); + electronLog.log('[KCC-Update] Latest release:', remoteVersion, '| Current:', currentVersion); + + if (!remoteVersion || !versionLessThan(currentVersion, remoteVersion)) { + electronLog.log('[KCC-Update] Already up to date'); + resolve(null); + return; + } + + const assets: Array<{ name: string; browser_download_url: string; size: number }> = release.assets || []; + const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name)); + if (!setupAsset) { + electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion); + resolve(null); + return; + } + + electronLog.log('[KCC-Update] Update available:', remoteVersion, '| Download:', setupAsset.browser_download_url, '| Size:', setupAsset.size); + resolve({ + version: remoteVersion, + downloadUrl: setupAsset.browser_download_url, + fileSize: setupAsset.size, + }); + } catch (err) { + electronLog.error('[KCC-Update] Failed to parse release data:', err); + resolve(null); + } + }); + res.on('error', (err) => { + electronLog.error('[KCC-Update] Response error:', err); + resolve(null); + }); + } + + req.setTimeout(CHECK_TIMEOUT_MS, () => { + electronLog.error('[KCC-Update] Check timed out after', CHECK_TIMEOUT_MS, 'ms'); + req.destroy(); + resolve(null); + }); + + req.on('error', (err) => { + electronLog.error('[KCC-Update] Check error:', err); + resolve(null); + }); + }); +} + +export function downloadUpdate(url: string, destPath: string, onProgress: ProgressCallback): Promise { + return new Promise((resolve, reject) => { + const tmpPath = destPath + '.tmp'; + + function doDownload(downloadUrl: string): void { + electronLog.log('[KCC-Update] Downloading from:', downloadUrl); + const req = httpsGet(downloadUrl, { + rejectUnauthorized: UPDATE_CONFIG.rejectUnauthorized, + headers: { 'User-Agent': 'KrunkerCivilianClient' }, + }, (res) => { + // Follow redirects + if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + electronLog.log('[KCC-Update] Download redirected to:', res.headers.location); + doDownload(res.headers.location); + return; + } + + if (res.statusCode !== 200) { + electronLog.error('[KCC-Update] Download returned status', res.statusCode, 'from:', downloadUrl); + reject(new Error('Download returned status ' + res.statusCode)); + return; + } + + const total = parseInt(res.headers['content-length'] || '0', 10); + let received = 0; + + const file = createWriteStream(tmpPath); + res.on('data', (chunk: Buffer) => { + received += chunk.length; + if (total > 0) { + onProgress(Math.round(100 * received / total)); + } + }); + res.pipe(file); + + file.on('finish', () => { + file.close(() => { + try { + if (existsSync(destPath)) unlinkSync(destPath); + renameSync(tmpPath, destPath); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + + file.on('error', (err) => { + try { unlinkSync(tmpPath); } catch { /* ignore */ } + reject(err); + }); + + res.on('error', (err) => { + try { unlinkSync(tmpPath); } catch { /* ignore */ } + reject(err); + }); + }); + + req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => { + req.destroy(); + try { unlinkSync(tmpPath); } catch { /* ignore */ } + reject(new Error('Download timed out')); + }); + + req.on('error', (err) => { + try { unlinkSync(tmpPath); } catch { /* ignore */ } + reject(err); + }); + } + + doDownload(url); + }); +} + +export function installUpdate(installerPath: string): void { + electronLog.log('[KCC-Update] Launching installer:', installerPath); + const child = spawn(installerPath, [], { + detached: true, + stdio: 'ignore', + }); + child.unref(); + app.quit(); +} diff --git a/src/main/userscripts.ts b/src/main/userscripts.ts new file mode 100644 index 0000000..cf92f91 --- /dev/null +++ b/src/main/userscripts.ts @@ -0,0 +1,99 @@ +import { mkdirSync, promises as fsp } from 'fs'; +import { join, parse } from 'path'; + +export interface ScriptFile { + filename: string; + content: string; + fullpath: string; +} + +export type ScriptTracker = Record; + +/** + * Manages userscript files, tracker state, and per-script preferences. + * Scripts live in a `scripts/` subdirectory; tracker.json records enabled/disabled state; + * per-script preferences are stored in `scripts/preferences/.json`. + */ +export class UserscriptManager { + private scriptsDir: string; + private prefsDir: string; + private trackerPath: string; + + constructor(baseDir: string) { + this.scriptsDir = join(baseDir, 'scripts'); + this.prefsDir = join(this.scriptsDir, 'preferences'); + this.trackerPath = join(this.scriptsDir, 'tracker.json'); + mkdirSync(this.scriptsDir, { recursive: true }); + mkdirSync(this.prefsDir, { recursive: true }); + } + + get dir(): string { + return this.scriptsDir; + } + + /** Read all .js files from the scripts directory */ + async scanScripts(): Promise { + const scripts: ScriptFile[] = []; + try { + for (const entry of await fsp.readdir(this.scriptsDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith('.js')) continue; + const fullpath = join(this.scriptsDir, entry.name); + try { + const content = await fsp.readFile(fullpath, 'utf-8'); + scripts.push({ filename: entry.name, content, fullpath }); + } catch { /* skip unreadable files */ } + } + } catch { /* directory read failed */ } + return scripts; + } + + /** Load tracker.json, add new scripts as disabled, prune deleted scripts */ + async loadTracker(scripts: ScriptFile[]): Promise { + let tracker: ScriptTracker = {}; + try { + tracker = JSON.parse(await fsp.readFile(this.trackerPath, 'utf-8')); + } catch { tracker = {}; } + + const filenames = new Set(scripts.map(s => s.filename)); + let dirty = false; + + // Add new scripts as disabled + for (const name of filenames) { + if (!(name in tracker)) { tracker[name] = false; dirty = true; } + } + + // Prune deleted scripts + for (const name of Object.keys(tracker)) { + if (!filenames.has(name)) { delete tracker[name]; dirty = true; } + } + + if (dirty) await this.saveTracker(tracker); + return tracker; + } + + /** Write tracker.json */ + async saveTracker(tracker: ScriptTracker): Promise { + try { + await fsp.writeFile(this.trackerPath, JSON.stringify(tracker, null, 2), 'utf-8'); + } catch { /* write failed */ } + } + + /** Load per-script preferences from preferences/.json */ + async loadScriptPrefs(filename: string): Promise> { + const name = parse(filename).name; + const prefsPath = join(this.prefsDir, name + '.json'); + try { + return JSON.parse(await fsp.readFile(prefsPath, 'utf-8')); + } catch { /* parse failed or file not found */ } + return {}; + } + + /** Save per-script preferences to preferences/.json */ + async saveScriptPrefs(filename: string, prefs: Record): Promise { + const name = parse(filename).name; + const prefsPath = join(this.prefsDir, name + '.json'); + try { + await fsp.writeFile(prefsPath, JSON.stringify(prefs, null, 2), 'utf-8'); + } catch { /* write failed */ } + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..f812ba3 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,1652 @@ +import { ipcRenderer } from 'electron'; +import { fetchGame, MATCHMAKER_GAMEMODES, MATCHMAKER_REGIONS, MATCHMAKER_REGION_NAMES } from './matchmaker'; +import type { MatchmakerConfig } from './matchmaker'; +import { initUserscripts, getInstances, setScriptEnabled } from './userscripts'; +import type { UserscriptInstance, UserscriptSetting } from './userscripts'; +import { initTranslator, updateTranslatorConfig } from './translator'; +import { setDeathAnimBlock, escapeHtml } from './utils'; + + +// ── Save console methods before Krunker overwrites them ── +// Wrapped to forward errors/warnings always, and logs when verbose is enabled +let _verboseLogging = false; + +const _console = { + log: (...args: unknown[]) => { + console.log(...args); + if (_verboseLogging) ipcRenderer.send('verbose-log', 'log', ...args); + }, + warn: (...args: unknown[]) => { + console.warn(...args); + ipcRenderer.send('verbose-log', 'warn', ...args); + }, + error: (...args: unknown[]) => { + console.error(...args); + ipcRenderer.send('verbose-log', 'error', ...args); + }, +}; + +_console.log('[KCC] Preload script loaded'); + +// ── Krunker-native settings styling constants (from Crankshaft) ── +const SAFETY_SVG = ''; +const REFRESH_SVG = ''; +const SAFETY_DESCS = [ + 'This setting is safe/standard', + 'Proceed with caution', + 'This setting is not recommended', + 'This setting is experimental', + 'This setting is experimental and unstable. Use at your own risk.', +]; + +const enum RefreshLevel { none, refresh, restart } +let refreshLevel: number = RefreshLevel.none; +let refreshPopupEl: HTMLElement | null = null; + +function safetyIcon(safety: string): string { + return '' + SAFETY_SVG + ''; +} + +function refreshIcon(mode: 'instant' | 'refresh-icon'): string { + return '' + REFRESH_SVG + ''; +} + +function restartIcon(): string { + return '' + SAFETY_SVG + ''; +} + +function settingIcon(safety: number, instant?: boolean, refreshOnly?: boolean, restart?: boolean): string { + if (safety > 0) return safetyIcon(SAFETY_DESCS[safety]); + if (instant) return refreshIcon('instant'); + if (refreshOnly) return refreshIcon('refresh-icon'); + if (restart) return restartIcon(); + return ''; +} + +function onSettingChanged(level: 'refresh' | 'restart'): void { + const newLevel = level === 'restart' ? RefreshLevel.restart : RefreshLevel.refresh; + if (newLevel > refreshLevel) refreshLevel = newLevel; + updateRefreshNotification(); +} + +function updateRefreshNotification(): void { + if (refreshLevel === RefreshLevel.none) { + if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; } + return; + } + if (refreshPopupEl) { try { refreshPopupEl.remove(); } catch (_e) { /* noop */ } } + refreshPopupEl = document.createElement('div'); + refreshPopupEl.className = 'kpc-holder-update refresh-popup'; + if (refreshLevel === RefreshLevel.restart) { + refreshPopupEl.innerHTML = 'Restart client fully to see changes'; + } else { + refreshPopupEl.innerHTML = '' + refreshIcon('refresh-icon') + 'Reload page with F5 or CTRL + R to see changes'; + } + document.body.appendChild(refreshPopupEl); +} + +// ── Tell Krunker this is a client (enables "Client" settings tab) ── +(window as any).OffCliV = true; + +// ── IPC bridge exposed as window.kpc ── +(window as any).kpc = { + platform: { + getInfo: () => ipcRenderer.invoke('get-platform'), + }, + config: { + get: (key: string) => ipcRenderer.invoke('get-config', key), + getAll: (keys: string[]) => ipcRenderer.invoke('get-all-config', keys), + set: (key: string, value: unknown) => ipcRenderer.invoke('set-config', key, value), + }, + window: { + minimize: () => ipcRenderer.invoke('window-minimize'), + maximize: () => ipcRenderer.invoke('window-maximize'), + close: () => ipcRenderer.invoke('window-close'), + isMaximized: () => ipcRenderer.invoke('window-is-maximized'), + }, + dev: { + toggleDevTools: () => ipcRenderer.invoke('toggle-devtools'), + }, + swapper: { + openFolder: () => ipcRenderer.invoke('open-swap-folder'), + getPath: () => ipcRenderer.invoke('get-swap-dir'), + }, + userscripts: { + openFolder: () => ipcRenderer.invoke('userscripts-open-folder'), + getPath: () => ipcRenderer.invoke('userscripts-get-dir'), + }, +}; + +// ── Client settings tab in Krunker's settings ── + +function hasOwn(obj: any, key: string): boolean { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +// ── Keybind types + helpers ── +interface Keybind { key: string; ctrl: boolean; shift: boolean; alt: boolean; } + +function keybindDisplayString(bind: Keybind): string { + return (bind.shift ? 'Shift+' : '') + (bind.ctrl ? 'Ctrl+' : '') + (bind.alt ? 'Alt+' : '') + bind.key.toUpperCase(); +} + +// ── Keybind capture dialog (Crankshaft-style) ── +let capturingKeybind: { resolve: (bind: Keybind) => void } | null = null; + +const kbOverlay = document.createElement('div'); +kbOverlay.className = 'kpc-keybind-overlay'; +const kbDialog = document.createElement('div'); +kbDialog.className = 'kpc-keybind-dialog'; +const kbTitle = document.createElement('div'); +kbTitle.className = 'kpc-keybind-dialog-title'; +const kbSub = document.createElement('div'); +kbSub.className = 'kpc-keybind-dialog-sub'; +kbSub.innerHTML = 'Press any key. Press Shift+Escape to cancel.'; +const kbModifiers = document.createElement('div'); +kbModifiers.className = 'kpc-keybind-dialog-modifiers'; +const kbShift = document.createElement('div'); +kbShift.className = 'kpc-keybind-modifier'; +kbShift.textContent = 'Shift'; +const kbCtrl = document.createElement('div'); +kbCtrl.className = 'kpc-keybind-modifier'; +kbCtrl.textContent = 'Control'; +const kbAlt = document.createElement('div'); +kbAlt.className = 'kpc-keybind-modifier'; +kbAlt.textContent = 'Alt'; +const kbCancel = document.createElement('div'); +kbCancel.className = 'kpc-keybind-dialog-cancel'; +kbCancel.textContent = 'Cancel'; +kbCancel.addEventListener('click', dismissKeybindDialog); + +kbModifiers.appendChild(kbShift); +kbModifiers.appendChild(kbCtrl); +kbModifiers.appendChild(kbAlt); +kbDialog.appendChild(kbCancel); +kbDialog.appendChild(kbTitle); +kbDialog.appendChild(kbSub); +kbDialog.appendChild(kbModifiers); +kbOverlay.appendChild(kbDialog); + +function dismissKeybindDialog(): void { + kbShift.classList.remove('active'); + kbCtrl.classList.remove('active'); + kbAlt.classList.remove('active'); + document.removeEventListener('keydown', kbKeydownHandler, true); + document.removeEventListener('keyup', kbKeyupHandler, true); + if (kbOverlay.parentNode) kbOverlay.remove(); + capturingKeybind = null; +} + +function kbKeydownHandler(event: KeyboardEvent): void { + event.stopImmediatePropagation(); + event.preventDefault(); + if (event.key === 'Control') kbCtrl.classList.add('active'); + else if (event.key === 'Shift') kbShift.classList.add('active'); + else if (event.key === 'Alt') kbAlt.classList.add('active'); +} + +function kbKeyupHandler(event: KeyboardEvent): void { + event.stopImmediatePropagation(); + event.preventDefault(); + if (!capturingKeybind) return; + + if (event.key === 'Escape' && event.shiftKey) { + dismissKeybindDialog(); + return; + } + + // Modifier-only releases just clear indicators + if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt') { + const bind: Keybind = { key: event.key, ctrl: false, shift: false, alt: false }; + capturingKeybind.resolve(bind); + dismissKeybindDialog(); + return; + } + + const bind: Keybind = { + key: event.key, + ctrl: event.ctrlKey, + shift: event.shiftKey, + alt: event.altKey, + }; + capturingKeybind.resolve(bind); + dismissKeybindDialog(); +} + +function openKeybindDialog(title: string): Promise { + return new Promise((resolve) => { + capturingKeybind = { resolve }; + kbTitle.textContent = 'Edit Keybind: ' + title; + kbShift.classList.remove('active'); + kbCtrl.classList.remove('active'); + kbAlt.classList.remove('active'); + document.addEventListener('keydown', kbKeydownHandler, true); + document.addEventListener('keyup', kbKeyupHandler, true); + const uiBase = document.getElementById('uiBase'); + if (uiBase) uiBase.appendChild(kbOverlay); + }); +} + +function createKeybindRow(label: string, desc: string, currentBind: Keybind, onBind: (bind: Keybind) => void, safety?: number, instant?: boolean): HTMLElement { + const s = safety || 0; + const row = document.createElement('div'); + row.className = 'setting settName safety-' + s + ' keybind'; + row.innerHTML = + settingIcon(s, instant) + + '' + label + '' + + '' + keybindDisplayString(currentBind) + '' + + '
' + desc + '
'; + const keyEl = row.querySelector('.kpc-keyIcon') as HTMLElement; + keyEl.addEventListener('click', () => { + openKeybindDialog(label).then((newBind) => { + keyEl.textContent = keybindDisplayString(newBind); + onBind(newBind); + }); + }); + return row; +} + +function createToggleRow(opts: { + label: string; + desc: string; + checked: boolean; + onChange: (checked: boolean) => void; + restart?: boolean; + disabled?: boolean; + safety?: number; + instant?: boolean; + refreshOnly?: boolean; +}): HTMLElement { + const s = opts.safety || 0; + const row = document.createElement('div'); + row.className = 'setting settName safety-' + s + ' bool'; + row.innerHTML = + settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) + + '' + opts.label + '' + + '' + + '
' + opts.desc + '
'; + if (!opts.disabled) { + const cb = row.querySelector('input[type="checkbox"]') as HTMLInputElement; + cb.addEventListener('change', () => { + opts.onChange(cb.checked); + if (opts.restart) onSettingChanged('restart'); + else if (opts.refreshOnly) onSettingChanged('refresh'); + }); + } + return row; +} + +function createSelectRow(opts: { + label: string; + desc: string; + options: Array<{ value: string; label: string }>; + value: string; + onChange: (value: string) => void; + restart?: boolean; + safety?: number; + instant?: boolean; + refreshOnly?: boolean; +}): HTMLElement { + const s = opts.safety || 0; + const row = document.createElement('div'); + row.className = 'setting settName safety-' + s + ' sel'; + row.innerHTML = + settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) + + '' + opts.label + '' + + '
' + opts.desc + '
'; + const select = document.createElement('select'); + select.className = 's-update inputGrey2'; + for (const o of opts.options) { + const option = document.createElement('option'); + option.value = o.value; + option.textContent = o.label; + if (o.value === opts.value) option.selected = true; + select.appendChild(option); + } + select.addEventListener('change', () => { + opts.onChange(select.value); + if (opts.restart) onSettingChanged('restart'); + else if (opts.refreshOnly) onSettingChanged('refresh'); + }); + row.appendChild(select); + return row; +} + +function createNumberRow(opts: { + label: string; + desc: string; + min: number; + max: number; + value: number; + onChange: (value: number) => void; + safety?: number; + restart?: boolean; + instant?: boolean; + refreshOnly?: boolean; +}): HTMLElement { + const s = opts.safety || 0; + const row = document.createElement('div'); + row.className = 'setting settName safety-' + s + ' num'; + row.innerHTML = + settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) + + '' + opts.label + '' + + '' + + '
' + + '' + + '
' + + '
' + opts.desc + '
'; + const rangeInput = row.querySelector('input[type="range"]') as HTMLInputElement; + const numInput = row.querySelector('input[type="number"]') as HTMLInputElement; + rangeInput.addEventListener('input', () => { + numInput.value = rangeInput.value; + }); + rangeInput.addEventListener('change', () => { + const v = Math.max(opts.min, Math.min(opts.max, parseInt(rangeInput.value) || 0)); + rangeInput.value = String(v); + numInput.value = String(v); + opts.onChange(v); + if (opts.restart) onSettingChanged('restart'); + else if (opts.refreshOnly) onSettingChanged('refresh'); + }); + numInput.addEventListener('change', () => { + const v = Math.max(opts.min, Math.min(opts.max, parseInt(numInput.value) || 0)); + numInput.value = String(v); + rangeInput.value = String(v); + opts.onChange(v); + if (opts.restart) onSettingChanged('restart'); + else if (opts.refreshOnly) onSettingChanged('refresh'); + }); + return row; +} + +function createCheckboxGrid(opts: { + header: string; + items: Array<{ value: string; label: string }>; + selected: string[]; + onChange: (selected: string[]) => void; +}): HTMLElement { + const row = document.createElement('div'); + row.className = 'setting settName safety-0 multisel'; + row.innerHTML = '' + opts.header + ''; + const grid = document.createElement('div'); + grid.className = 'kpc-multisel-parent'; + for (const item of opts.items) { + const label = document.createElement('label'); + label.className = 'hostOpt'; + label.innerHTML = + '' + item.label + '' + + '' + + '
'; + const cb = label.querySelector('input') as HTMLInputElement; + cb.addEventListener('change', () => { + if (cb.checked) { + if (!opts.selected.includes(item.value)) opts.selected.push(item.value); + } else { + const idx = opts.selected.indexOf(item.value); + if (idx >= 0) opts.selected.splice(idx, 1); + } + opts.onChange(opts.selected); + }); + grid.appendChild(label); + } + row.appendChild(grid); + return row; +} + +function hookSettings(): void { + const w = window as any; + const settingsWindow = w.windows[0]; + let selectedTab: number = settingsWindow.tabIndex; + + function isClientTab(): boolean { + const tabs = settingsWindow.tabs[settingsWindow.settingType]; + return tabs && selectedTab === tabs.length - 1; + } + + function safeRender(): void { + if (isClientTab()) renderSettings(); + } + + const origShowWindow = w.showWindow.bind(w); + const origChangeTab = settingsWindow.changeTab.bind(settingsWindow); + const origSearchList = settingsWindow.searchList.bind(settingsWindow); + + w.showWindow = (...args: unknown[]) => { + const result = origShowWindow(...args); + if (args[0] === 1) { + if (settingsWindow.settingType === 'basic') { + settingsWindow.toggleType({ checked: true }); + } + const advSlider = document.querySelector('.advancedSwitch input#typeBtn') as HTMLInputElement | null; + if (advSlider) { + advSlider.disabled = true; + if (advSlider.nextElementSibling) { + advSlider.nextElementSibling.setAttribute('title', 'Client auto-enables advanced settings mode'); + } + } + + const searchInput = document.getElementById('settSearch') as HTMLInputElement | null; + const searchQuery = searchInput?.value?.trim() ?? ''; + if (searchQuery.length > 0) renderSettings(searchQuery); + else if (isClientTab()) renderSettings(); + } + return result; + }; + + settingsWindow.changeTab = (...args: unknown[]) => { + const result = origChangeTab(...args); + selectedTab = settingsWindow.tabIndex; + safeRender(); + return result; + }; + + settingsWindow.searchList = (...args: unknown[]) => { + const result = origSearchList(...args); + const searchInput = document.getElementById('settSearch') as HTMLInputElement | null; + const query = searchInput?.value?.trim() ?? ''; + if (query.length > 0) { + renderSettings(query); + } else { + const existing = document.querySelector('#settHolder .kpc-settings'); + if (existing && !isClientTab()) existing.remove(); + else if (isClientTab()) renderSettings(); + } + return result; + }; + + safeRender(); +} + +function createSection(title: string, collapsed?: boolean): { section: HTMLElement; body: HTMLElement } { + const section = document.createElement('div'); + const header = document.createElement('div'); + header.className = 'setHed'; + header.innerHTML = '' + (collapsed ? 'keyboard_arrow_right' : 'keyboard_arrow_down') + '' + title; + const body = document.createElement('div'); + body.className = 'setBodH' + (collapsed ? ' setting-category-collapsed' : ''); + header.addEventListener('click', () => { + const isCollapsed = body.classList.toggle('setting-category-collapsed'); + const arrow = header.querySelector('.plusOrMinus'); + if (arrow) arrow.textContent = isCollapsed ? 'keyboard_arrow_right' : 'keyboard_arrow_down'; + }); + section.appendChild(header); + section.appendChild(body); + return { section, body }; +} + +// ── Settings section builders ── + +interface SettingsBag { + binds: Record; + saveBinds: () => void; + isWindows: boolean; +} + +function buildGeneralSection( + body: HTMLElement, gameConf: any, uiConfRaw: any, perfConf: any, bag: SettingsBag, +): void { + const perfDefaults = { fpsUnlocked: true }; + const perf = { ...perfDefaults, ...perfConf }; + + body.appendChild(createToggleRow({ + label: 'Unlimited FPS', + desc: 'Uncap the frame rate (requires restart)', + checked: perf.fpsUnlocked, restart: true, + onChange: (v) => { perf.fpsUnlocked = v; ipcRenderer.invoke('set-config', 'performance', perf); }, + })); + + const gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window' }; + const game = { ...gameDefaults, ...gameConf }; + + body.appendChild(createSelectRow({ + label: 'Social/Hub Tab Behaviour', + desc: 'How social, market, and editor pages open when clicked', + options: [{ value: 'New Window', label: 'New Window' }, { value: 'Same Window', label: 'Same Window' }], + value: game.socialTabBehaviour, instant: true, + onChange: (v) => { game.socialTabBehaviour = v; ipcRenderer.invoke('set-config', 'game', game); }, + })); + + const uiDefaults = { showExitButton: true, deathscreenAnimation: false, hideMenuPopups: false }; + const ui = { ...uiDefaults, ...uiConfRaw }; + + function saveUI(): void { + ipcRenderer.invoke('set-config', 'ui', ui); + } + + body.appendChild(createToggleRow({ + label: 'Show Exit Button', + desc: 'Show the exit button in the game sidebar', + checked: ui.showExitButton, instant: true, + onChange: (v) => { + ui.showExitButton = v; saveUI(); + const btn = document.getElementById('clientExit'); + if (btn) btn.style.display = v ? 'flex' : 'none'; + }, + })); + + body.appendChild(createToggleRow({ + label: 'Block Death Screen Animation', + desc: 'Disable the slide-in animation on the death screen', + checked: ui.deathscreenAnimation, instant: true, + onChange: (v) => { ui.deathscreenAnimation = v; saveUI(); setDeathAnimBlock(v); }, + })); + + body.appendChild(createToggleRow({ + label: 'Hide Menu Popups', + desc: 'Hide promotional notifications, offers, and streams on the main menu', + checked: ui.hideMenuPopups, instant: true, + onChange: (v) => { + ui.hideMenuPopups = v; saveUI(); + if (v) startHidePopups(); else stopHidePopups(); + }, + })); + + body.appendChild(createToggleRow({ + label: 'Join as Spectator', + desc: 'Automatically enable spectate mode when joining a game', + checked: game.joinAsSpectator, instant: true, + onChange: (v) => { game.joinAsSpectator = v; ipcRenderer.invoke('set-config', 'game', game); }, + })); + + if (ui.deathscreenAnimation) setDeathAnimBlock(true); + if (ui.hideMenuPopups) startHidePopups(); + + body.appendChild(createKeybindRow('Pause Chat', 'Freeze chat auto-scroll to read history (default F10)', bag.binds.pauseChat, (b) => { + bag.binds.pauseChat = b; + bag.saveBinds(); + }, undefined, true)); + + body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => { + bag.binds.fullscreenToggle = b; + bag.saveBinds(); + }, undefined, true)); +} + +function buildSwapperSection(body: HTMLElement, swapperConf: any): void { + const swapEnabled = swapperConf ? swapperConf.enabled : true; + + body.appendChild(createToggleRow({ + label: 'Resource Swapper', + desc: 'Replace game textures, sounds, and models with local files', + checked: swapEnabled, + restart: true, + onChange: (v) => { + ipcRenderer.invoke('get-config', 'swapper').then((conf: any) => { + ipcRenderer.invoke('set-config', 'swapper', { enabled: v, path: conf ? conf.path : '' }); + }); + }, + })); + + const folderRow = document.createElement('div'); + folderRow.className = 'setting settName safety-0 has-button'; + folderRow.innerHTML = + 'Swapper Folder' + + '
Place replacement assets here (textures/, sound/, models/)
'; + const swapFolderBtn = document.createElement('div'); + swapFolderBtn.className = 'settingsBtn'; + swapFolderBtn.title = 'Open Folder'; + swapFolderBtn.innerHTML = 'folder Swapper'; + swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder')); + folderRow.appendChild(swapFolderBtn); + body.appendChild(folderRow); +} + +function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void { + const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true }; + + function saveMM(): void { + ipcRenderer.invoke('set-config', 'matchmaker', mm); + } + + body.appendChild(createToggleRow({ + label: 'Custom Matchmaker', + desc: 'Press F6 to find a game matching your criteria', + checked: mm.enabled, instant: true, + onChange: (v) => { mm.enabled = v; saveMM(); }, + })); + + body.appendChild(createToggleRow({ + label: 'Open Server Browser on Cancel', + desc: 'Opens the server browser when no game is found and you cancel', + checked: mm.openServerBrowser, instant: true, + onChange: (v) => { mm.openServerBrowser = v; saveMM(); }, + })); + + body.appendChild(createKeybindRow('Matchmaker Hotkey', 'Key to trigger the custom matchmaker', bag.binds.matchmaker, (b) => { + bag.binds.matchmaker = b; + bag.saveBinds(); + }, undefined, true)); + body.appendChild(createKeybindRow('Matchmaker Accept', 'Key to accept a found game', bag.binds.matchmakerAccept, (b) => { + bag.binds.matchmakerAccept = b; + bag.saveBinds(); + }, undefined, true)); + body.appendChild(createKeybindRow('Matchmaker Cancel', 'Key to dismiss the matchmaker popup', bag.binds.matchmakerCancel, (b) => { + bag.binds.matchmakerCancel = b; + bag.saveBinds(); + }, undefined, true)); + + body.appendChild(createNumberRow({ + label: 'Min Players', desc: 'Minimum player count in lobby (0-7)', + min: 0, max: 7, value: mm.minPlayers, instant: true, + onChange: (v) => { mm.minPlayers = v; saveMM(); }, + })); + + body.appendChild(createNumberRow({ + label: 'Max Players', desc: 'Maximum player count in lobby (0-7)', + min: 0, max: 7, value: mm.maxPlayers, instant: true, + onChange: (v) => { mm.maxPlayers = v; saveMM(); }, + })); + + body.appendChild(createNumberRow({ + label: 'Min Remaining Time', desc: 'Minimum seconds remaining in match (0-480)', + min: 0, max: 480, value: mm.minRemainingTime, instant: true, + onChange: (v) => { mm.minRemainingTime = v; saveMM(); }, + })); + + body.appendChild(createCheckboxGrid({ + header: 'Regions (none selected = all)', + items: MATCHMAKER_REGIONS.map(r => ({ value: r, label: MATCHMAKER_REGION_NAMES[r] || r })), + selected: mm.regions, + onChange: () => saveMM(), + })); + + body.appendChild(createCheckboxGrid({ + header: 'Gamemodes (none selected = all)', + items: MATCHMAKER_GAMEMODES.map(gm => ({ value: gm, label: gm })), + selected: mm.gamemodes, + onChange: () => saveMM(), + })); +} + +function buildDiscordSection(body: HTMLElement, discordConf: any): void { + const discord = { enabled: false, ...discordConf }; + + body.appendChild(createToggleRow({ + label: 'Discord Rich Presence', + desc: 'Show game activity in your Discord profile', + checked: discord.enabled, + restart: true, + onChange: (v) => { + discord.enabled = v; + ipcRenderer.invoke('set-config', 'discord', discord); + }, + })); +} + +// ── Alt Manager helpers ── +function encodeCredential(decoded: string): string { + const key = decoded.length; + return encodeURIComponent( + decoded.split('').map(c => String.fromCharCode(c.charCodeAt(0) + key)).join('') + ); +} + +function decodeCredential(encoded: string): string { + const str = decodeURIComponent(encoded); + const key = str.length; + return str.split('').map(c => String.fromCharCode(c.charCodeAt(0) - key)).join(''); +} + +function switchToAccount(account: { username: string; password: string }): void { + const w = window as any; + if (typeof w.loginOrRegister !== 'function') return; + + function doLogin(): void { + w.loginOrRegister(); + queueMicrotask(() => { + const toggleBtn = document.querySelector('.auth-toggle-btn') as HTMLElement; + if (toggleBtn && toggleBtn.textContent?.includes('username')) toggleBtn.click(); + queueMicrotask(() => { + const nameInput = document.querySelector('#accName') as HTMLInputElement; + const passInput = document.querySelector('#accPass') as HTMLInputElement; + if (!nameInput || !passInput) return; + nameInput.value = decodeCredential(account.username); + passInput.value = decodeCredential(account.password); + nameInput.dispatchEvent(new Event('input', { bubbles: true })); + passInput.dispatchEvent(new Event('input', { bubbles: true })); + const submitBtn = document.querySelector('.io-button') as HTMLElement; + if (submitBtn) submitBtn.click(); + }); + }); + } + + if (typeof w.logoutAcc === 'function') { + w.logoutAcc(); + setTimeout(doLogin, 500); + } else { + doLogin(); + } +} + +function buildAccountsSection(body: HTMLElement, accountsArr: any[]): void { + const accounts: any[] = accountsArr || []; + + const addBtn = document.createElement('div'); + addBtn.className = 'setting settName safety-0 has-button'; + addBtn.innerHTML = + 'Add Account' + + '' + + '
Save a Krunker account for quick switching
'; + body.appendChild(addBtn); + + const form = document.createElement('div'); + form.className = 'kpc-acc-form'; + form.style.display = 'none'; + form.innerHTML = + '' + + '' + + '' + + '
' + + '' + + '' + + '
'; + body.appendChild(form); + + const labelIn = form.querySelector('.kpc-acc-label') as HTMLInputElement; + const userIn = form.querySelector('.kpc-acc-user') as HTMLInputElement; + const passIn = form.querySelector('.kpc-acc-pass') as HTMLInputElement; + + addBtn.querySelector('button')!.addEventListener('click', () => { + form.style.display = form.style.display === 'none' ? '' : 'none'; + }); + + form.querySelector('.kpc-acc-cancel')!.addEventListener('click', () => { + form.style.display = 'none'; + }); + + const listEl = document.createElement('div'); + body.appendChild(listEl); + + function renderList(): void { + listEl.innerHTML = ''; + if (accounts.length === 0) { + listEl.innerHTML = '
No saved accounts
'; + return; + } + accounts.forEach((acc, i) => { + const row = document.createElement('div'); + row.className = 'kpc-acc-item'; + row.innerHTML = + '
' + + '' + escapeHtml(acc.label) + '' + + '
' + + '
' + + '' + + '' + + '
'; + row.querySelector('.kpc-acc-switch')!.addEventListener('click', () => switchToAccount(acc)); + row.querySelector('.kpc-acc-delete')!.addEventListener('click', () => { + ipcRenderer.invoke('alt-remove', i).then(() => { + accounts.splice(i, 1); + renderList(); + }); + }); + listEl.appendChild(row); + }); + } + renderList(); + + form.querySelector('.kpc-acc-save')!.addEventListener('click', () => { + const label = labelIn.value.trim(); + const user = userIn.value.trim(); + const pass = passIn.value; + if (!label || !user || !pass) return; + const newAcc = { + label, + username: encodeCredential(user), + password: encodeCredential(pass), + }; + ipcRenderer.invoke('alt-save', newAcc).then(() => { + accounts.push(newAcc); + labelIn.value = ''; + userIn.value = ''; + passIn.value = ''; + form.style.display = 'none'; + renderList(); + }); + }); +} + +function buildTranslatorSection(body: HTMLElement, translatorConf: any): void { + const tl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf }; + + function saveTL(): void { + ipcRenderer.invoke('set-config', 'translator', tl); + } + + body.appendChild(createToggleRow({ + label: 'Chat Translator', + desc: 'Automatically translate non-English chat messages', + checked: tl.enabled, instant: true, + onChange: (v) => { + tl.enabled = v; + saveTL(); + updateTranslatorConfig({ enabled: v }); + }, + })); + + body.appendChild(createSelectRow({ + label: 'Target Language', + desc: 'Language to translate messages into', instant: true, + options: [ + { value: 'en', label: 'English' }, + { value: 'es', label: 'Spanish' }, + { value: 'fr', label: 'French' }, + { value: 'de', label: 'German' }, + { value: 'pt', label: 'Portuguese' }, + { value: 'ru', label: 'Russian' }, + { value: 'ja', label: 'Japanese' }, + { value: 'ko', label: 'Korean' }, + { value: 'zh', label: 'Chinese' }, + { value: 'ar', label: 'Arabic' }, + { value: 'hi', label: 'Hindi' }, + { value: 'tr', label: 'Turkish' }, + { value: 'pl', label: 'Polish' }, + { value: 'it', label: 'Italian' }, + { value: 'nl', label: 'Dutch' }, + ], + value: tl.targetLanguage, + onChange: (v) => { + tl.targetLanguage = v; + saveTL(); + updateTranslatorConfig({ targetLanguage: v }); + }, + })); + + body.appendChild(createToggleRow({ + label: 'Show Language Tag', + desc: 'Show detected language code before translations (e.g. [FR])', + checked: tl.showLanguageTag, instant: true, + onChange: (v) => { + tl.showLanguageTag = v; + saveTL(); + updateTranslatorConfig({ showLanguageTag: v }); + }, + })); +} + +function buildAdvancedSection( + body: HTMLElement, advConf: any, isWindows: boolean, +): void { + const advDefaults = { + removeUselessFeatures: true, + gpuRasterizing: false, + helpfulFlags: true, + disableAccelerated2D: false, + increaseLimits: false, + lowLatency: false, + experimentalFlags: false, + angleBackend: 'default', + }; + const adv = { ...advDefaults, ...advConf }; + + function saveAdv(): void { + ipcRenderer.invoke('set-config', 'advanced', adv); + } + + const angleOptions: Array<{ value: string; label: string }> = isWindows + ? [ + { value: 'default', label: 'Default (D3D11)' }, + { value: 'gl', label: 'OpenGL' }, + { value: 'd3d9', label: 'Direct3D 9' }, + { value: 'd3d11', label: 'Direct3D 11' }, + { value: 'd3d11on12', label: 'D3D11on12' }, + { value: 'vulkan', label: 'Vulkan' }, + ] + : [ + { value: 'default', label: 'Default' }, + { value: 'gl', label: 'OpenGL' }, + { value: 'vulkan', label: 'Vulkan' }, + ]; + + body.appendChild(createSelectRow({ + label: 'ANGLE Backend', + desc: 'Graphics API used for WebGL rendering', + options: angleOptions, + value: adv.angleBackend, restart: true, + onChange: (v) => { adv.angleBackend = v; saveAdv(); }, + })); + + const advToggles: Array<{ key: string; label: string; desc: string; safety: number }> = [ + { key: 'removeUselessFeatures', label: 'Remove Useless Features', desc: 'Disables crash reporting, metrics, print preview, and other unused Chromium features', safety: 1 }, + { key: 'gpuRasterizing', label: 'GPU Rasterization', desc: 'Force GPU rasterization and out-of-process rasterization', safety: 2 }, + { key: 'helpfulFlags', label: 'Useful Flags', desc: 'Enables WebGL, JS harmony, V8 features, background throttle prevention, and autoplay bypass', safety: 3 }, + { key: 'disableAccelerated2D', label: 'Disable Accelerated 2D Canvas', desc: 'Disables hardware-accelerated 2D canvas rendering', safety: 3 }, + { key: 'increaseLimits', label: 'Increase Limits', desc: 'Raises renderer process, WebGL context, and WebRTC CPU limits; ignores GPU blocklist', safety: 4 }, + { key: 'lowLatency', label: 'Low Latency Flags', desc: 'Enables high-resolution timer, QUIC protocol, and accelerated 2D canvas', safety: 4 }, + { key: 'experimentalFlags', label: 'Experimental Flags', desc: 'Enables accelerated video decode, native GPU memory buffers, high DPI support, and disables pings/proxy', safety: 4 }, + ]; + + for (const t of advToggles) { + body.appendChild(createToggleRow({ + label: t.label, desc: t.desc, + checked: !!adv[t.key], restart: true, + safety: t.safety, + onChange: (v) => { adv[t.key] = v; saveAdv(); }, + })); + } +} + +// ── Search filter + "no settings" cleanup ── +function applySearchFilter(container: HTMLElement, holder: HTMLElement, searchQuery: string): void { + const query = searchQuery.toLowerCase(); + const sections = Array.from(container.children).filter(el => el.querySelector('.setHed')); + sections.forEach(sectionEl => { + const sectionTitle = sectionEl.querySelector('.setHed')?.textContent?.toLowerCase() || ''; + const body = sectionEl.querySelector('.setBodH'); + if (!body) { (sectionEl as HTMLElement).style.display = 'none'; return; } + + if (sectionTitle.includes(query)) { + body.classList.remove('setting-category-collapsed'); + return; + } + + let visibleCount = 0; + Array.from(body.children).forEach(child => { + const el = child as HTMLElement; + const text = el.textContent?.toLowerCase() || ''; + if (text.includes(query)) { + el.style.display = ''; + visibleCount++; + } else { + el.style.display = 'none'; + } + }); + if (visibleCount === 0) { + (sectionEl as HTMLElement).style.display = 'none'; + } else { + body.classList.remove('setting-category-collapsed'); + } + }); + + const hasVisible = sections.find(el => (el as HTMLElement).style.display !== 'none'); + if (hasVisible) { + Array.from(holder.children).forEach(child => { + if ((child as HTMLElement).textContent?.toLowerCase().includes('no settings')) { + (child as HTMLElement).remove(); + } + }); + } +} + +function renderSettings(searchQuery?: string): void { + const holder = document.getElementById('settHolder'); + if (!holder) return; + + refreshLevel = RefreshLevel.none; + if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; } + + if (searchQuery) { + const existing = holder.querySelector('.kpc-settings'); + if (existing) existing.remove(); + } else { + while (holder.firstChild) holder.removeChild(holder.firstChild); + } + + const container = document.createElement('div'); + container.className = 'kpc-settings'; + + // ── Action button grid ── + const actionGrid = document.createElement('div'); + actionGrid.className = 'kpc-action-grid'; + + const actionButtons: Array<{ label: string; color: string; full?: boolean; action: () => void }> = [ + { label: 'Open Resource Swapper', color: 'kpc-ab-pink', action: () => ipcRenderer.invoke('open-swap-folder') }, + { label: 'Reset Resource Swapper', color: 'kpc-ab-pink', action: () => { + if (confirm('Reset resource swapper? This will delete all files in the swapper folder.')) { + ipcRenderer.invoke('reset-swapper'); + } + }}, + { label: 'Open Electron Logs', color: 'kpc-ab-red', action: () => ipcRenderer.invoke('open-electron-log') }, + { label: 'Restart Client', color: 'kpc-ab-orange', full: true, action: () => ipcRenderer.invoke('restart-client') }, + { label: 'Reset Options', color: 'kpc-ab-red', action: () => { + if (confirm('Reset all settings to defaults? The client will restart.')) { + ipcRenderer.invoke('reset-options'); + } + }}, + { label: 'Delete All Data', color: 'kpc-ab-red', action: () => { + if (confirm('Delete all data (config, logs)? Scripts are preserved. The client will restart.')) { + ipcRenderer.invoke('delete-all-data'); + } + }}, + ]; + + for (const ab of actionButtons) { + const btn = document.createElement('button'); + btn.className = 'kpc-action-btn ' + ab.color + (ab.full ? ' full' : ''); + btn.textContent = ab.label; + btn.addEventListener('click', ab.action); + actionGrid.appendChild(btn); + } + container.appendChild(actionGrid); + + // ── Create section shells ── + const genSec = createSection('General'); + container.appendChild(genSec.section); + const swapSec = createSection('Swapper'); + container.appendChild(swapSec.section); + const mmSec = createSection('Matchmaker'); + container.appendChild(mmSec.section); + const tlSec = createSection('Translator'); + container.appendChild(tlSec.section); + const discordSec = createSection('Discord'); + container.appendChild(discordSec.section); + const accSec = createSection('Accounts', true); + container.appendChild(accSec.section); + const advSec = createSection('Advanced'); + container.appendChild(advSec.section); + const usSec = createSection('Userscripts'); + container.appendChild(usSec.section); + + // Load all configs in a single IPC call + platform info + Promise.all([ + ipcRenderer.invoke('get-all-config', ['swapper', 'matchmaker', 'keybinds', 'advanced', 'game', 'ui', 'discord', 'translator', 'accounts', 'performance']), + ipcRenderer.invoke('get-platform'), + ]).then(([allConf, platformInfo]: [any, any]) => { + const swapperConf = allConf.swapper; + const mmConf = allConf.matchmaker; + const keybindsConf = allConf.keybinds; + const advConf = allConf.advanced; + const gameConf = allConf.game; + const uiConfRaw = allConf.ui; + const discordConf = allConf.discord; + const translatorConf = allConf.translator; + const defaultBinds = { + matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false }, + matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false }, + matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false }, + pauseChat: { key: 'F10', ctrl: false, shift: false, alt: false }, + fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false }, + }; + const binds = { ...defaultBinds, ...keybindsConf }; + const isWindows = platformInfo && platformInfo.isWindows; + + const bag: SettingsBag = { + binds, + saveBinds: () => ipcRenderer.invoke('set-config', 'keybinds', binds), + isWindows, + }; + + // Populate each section + buildGeneralSection(genSec.body, gameConf, uiConfRaw, allConf.performance, bag); + buildSwapperSection(swapSec.body, swapperConf); + buildMatchmakerSection(mmSec.body, mmConf, bag); + buildTranslatorSection(tlSec.body, translatorConf); + buildDiscordSection(discordSec.body, discordConf); + buildAccountsSection(accSec.body, allConf.accounts); + buildAdvancedSection(advSec.body, advConf, isWindows); + renderUserscriptsSection(usSec.body); + + if (searchQuery) applySearchFilter(container, holder, searchQuery); + + holder.appendChild(container); + }).catch((err: any) => { + console.error('[KCC] Settings render error:', err); + }); +} + +// ── Userscripts settings section ── +function renderUserscriptsSection(body: HTMLElement): void { + ipcRenderer.invoke('get-config', 'userscripts').then((usConf: any) => { + const us = usConf || { enabled: true, path: '' }; + + body.appendChild(createToggleRow({ + label: 'Userscripts', + desc: 'Load custom scripts from the scripts folder', + checked: us.enabled, restart: true, + onChange: (v) => { us.enabled = v; ipcRenderer.invoke('set-config', 'userscripts', us); }, + })); + + const usFolderRow = document.createElement('div'); + usFolderRow.className = 'setting settName safety-0 has-button'; + usFolderRow.innerHTML = + 'Scripts Folder' + + '
Place .js userscript files here
'; + const usFolderBtn = document.createElement('div'); + usFolderBtn.className = 'settingsBtn'; + usFolderBtn.title = 'Open Folder'; + usFolderBtn.innerHTML = 'folder Scripts'; + usFolderBtn.addEventListener('click', () => ipcRenderer.invoke('userscripts-open-folder')); + usFolderRow.appendChild(usFolderBtn); + body.appendChild(usFolderRow); + + const scriptInstances = getInstances(); + if (scriptInstances.length === 0) { + const emptyRow = document.createElement('div'); + emptyRow.className = 'setting settName safety-0'; + emptyRow.innerHTML = + '
No userscripts found. Place .js files in the scripts folder and reload.
'; + body.appendChild(emptyRow); + return; + } + + for (const inst of scriptInstances) { + const scriptRow = document.createElement('div'); + scriptRow.className = 'setting settName safety-0 bool'; + + const displayName = inst.meta.name || inst.filename; + let metaParts: string[] = []; + if (inst.meta.author) metaParts.push('by ' + inst.meta.author); + if (inst.meta.version) metaParts.push('v' + inst.meta.version); + const metaLine = metaParts.length > 0 ? '' + metaParts.join(' · ') + '' : ''; + const descText = inst.meta.desc || ''; + + scriptRow.innerHTML = + '' + displayName + '' + + '' + + '
' + descText + (metaLine ? '
' + metaLine : '') + '
'; + body.appendChild(scriptRow); + + const cb = scriptRow.querySelector('input[type="checkbox"]') as HTMLInputElement; + const settingsContainer = document.createElement('div'); + settingsContainer.className = 'kpc-us-settings'; + body.appendChild(settingsContainer); + + if (inst.enabled && inst.settings) { + renderScriptSettings(inst, settingsContainer); + } + + cb.addEventListener('change', () => { + const { needsReload } = setScriptEnabled(inst.filename, cb.checked, _console); + settingsContainer.innerHTML = ''; + if (cb.checked && inst.settings) { + renderScriptSettings(inst, settingsContainer); + } + if (needsReload) { + onSettingChanged('refresh'); + } + }); + } + }); +} + +function renderScriptSettings(inst: UserscriptInstance, container: HTMLElement): void { + if (!inst.settings) return; + + for (const [key, setting] of Object.entries(inst.settings)) { + const typeClass = setting.type === 'bool' ? 'bool' : setting.type === 'sel' ? 'sel' : setting.type === 'num' ? 'num' : setting.type === 'keybind' ? 'keybind' : ''; + const row = document.createElement('div'); + row.className = 'setting settName safety-0' + (typeClass ? ' ' + typeClass : ''); + row.innerHTML = + '' + setting.title + '' + + (setting.desc ? '
' + setting.desc + '
' : ''); + + switch (setting.type) { + case 'bool': { + const label = document.createElement('label'); + label.className = 'switch'; + label.innerHTML = + '' + + '
'; + row.appendChild(label); + const input = label.querySelector('input') as HTMLInputElement; + input.addEventListener('change', () => { + setting.value = input.checked; + if (typeof setting.changed === 'function') setting.changed(setting.value); + saveScriptSetting(inst, key); + }); + break; + } + case 'num': { + const input = document.createElement('input'); + input.type = 'number'; + input.className = 'rb-input s-update sliderVal'; + input.value = String(setting.value); + if (setting.min !== undefined) input.min = String(setting.min); + if (setting.max !== undefined) input.max = String(setting.max); + if (setting.step !== undefined) input.step = String(setting.step); + row.appendChild(input); + input.addEventListener('change', () => { + setting.value = parseFloat(input.value) || 0; + if (typeof setting.changed === 'function') setting.changed(setting.value); + saveScriptSetting(inst, key); + }); + break; + } + case 'sel': { + const select = document.createElement('select'); + select.className = 's-update inputGrey2'; + if (setting.opts) { + for (const opt of setting.opts) { + const option = document.createElement('option'); + option.value = String(opt); + option.textContent = String(opt); + if (String(opt) === String(setting.value)) option.selected = true; + select.appendChild(option); + } + } + row.appendChild(select); + select.addEventListener('change', () => { + setting.value = select.value; + if (typeof setting.changed === 'function') setting.changed(setting.value); + saveScriptSetting(inst, key); + }); + break; + } + case 'color': { + const input = document.createElement('input'); + input.type = 'color'; + input.className = 'kpc-color-input'; + input.value = String(setting.value) || '#ffffff'; + row.appendChild(input); + input.addEventListener('input', () => { + setting.value = input.value; + if (typeof setting.changed === 'function') setting.changed(setting.value); + saveScriptSetting(inst, key); + }); + break; + } + case 'keybind': { + const bind = setting.value as Keybind; + const keyEl = document.createElement('span'); + keyEl.className = 'keyIcon kpc-keyIcon'; + keyEl.textContent = keybindDisplayString(bind); + keyEl.addEventListener('click', () => { + openKeybindDialog(setting.title).then((newBind) => { + setting.value = newBind; + keyEl.textContent = keybindDisplayString(newBind); + if (typeof setting.changed === 'function') setting.changed(setting.value); + saveScriptSetting(inst, key); + }); + }); + row.appendChild(keyEl); + break; + } + } + + container.appendChild(row); + } +} + +function saveScriptSetting(inst: UserscriptInstance, _key: string): void { + if (!inst.settings) return; + const prefs: Record = {}; + for (const [k, s] of Object.entries(inst.settings)) { + prefs[k] = s.value; + } + ipcRenderer.invoke('userscripts-save-prefs', inst.filename, prefs); +} + +// ── Hide menu popups (polling-based, safe per MutationObserver constraint) ── +let _hidePopupsInterval: ReturnType | null = null; +const HIDE_POPUPS_CSS = + '#leftTabsHolder > .youNewDiv:not(#battlepassAd), .webpush-container, ' + + '#homeStoreAd, #streamContainerNew, #bundlePop, #genericPop.claimPop, ' + + '#newsHolder, #streamContainer { display: none !important; }'; +const HIDE_POPUPS_ELS = ['homeStoreAd', 'streamContainerNew']; + +function startHidePopups(): void { + if (_hidePopupsInterval) return; + if (!document.getElementById('kpc-hideMenuPopups')) { + const style = document.createElement('style'); + style.id = 'kpc-hideMenuPopups'; + style.textContent = HIDE_POPUPS_CSS; + document.head.appendChild(style); + } + const w = window as any; + _hidePopupsInterval = setInterval(() => { + for (const id of HIDE_POPUPS_ELS) { + const el = document.getElementById(id); + if (el && el.style.display !== 'none') el.style.display = 'none'; + } + const bundlePop = document.getElementById('bundlePop'); + if (bundlePop && bundlePop.children.length > 0 && bundlePop.style.display !== 'none') { + if (typeof w.clearPops === 'function') w.clearPops(); + } + const genericPop = document.getElementById('genericPop'); + if (genericPop && genericPop.classList.contains('claimPop') && genericPop.style.display !== 'none') { + if (typeof w.clearPops === 'function') w.clearPops(); + } + }, 1000); +} + +function stopHidePopups(): void { + if (_hidePopupsInterval) { clearInterval(_hidePopupsInterval); _hidePopupsInterval = null; } + const style = document.getElementById('kpc-hideMenuPopups'); + if (style) style.remove(); + for (const id of HIDE_POPUPS_ELS) { + const el = document.getElementById(id); + if (el) el.style.display = ''; + } +} + +// ── Matchmaker IPC listener ── +ipcRenderer.on('matchmaker-find', (_e, mmConfig: MatchmakerConfig) => { + fetchGame(mmConfig, _console).catch((err) => _console.error('[KCC] Matchmaker error:', err)); +}); + +// ── Chat pause ── +let chatPaused = false; +let chatSavedScrollTop = 0; + +function onChatWheel(e: WheelEvent): void { + const chatList = document.getElementById('chatList'); + if (!chatList) return; + chatSavedScrollTop = Math.max(0, Math.min( + chatSavedScrollTop + e.deltaY, + chatList.scrollHeight - chatList.clientHeight, + )); + chatList.scrollTop = chatSavedScrollTop; +} + +ipcRenderer.on('toggle-chat-pause', () => { + const chatList = document.getElementById('chatList'); + if (!chatList) return; + + chatPaused = !chatPaused; + + if (chatPaused) { + chatSavedScrollTop = chatList.scrollTop; + chatList.classList.add('kpc-chat-paused'); + chatList.style.overflow = 'hidden'; + chatList.addEventListener('wheel', onChatWheel, { passive: true }); + } else { + chatList.classList.remove('kpc-chat-paused'); + chatList.style.overflow = ''; + chatList.removeEventListener('wheel', onChatWheel); + chatList.scrollTop = chatList.scrollHeight; + } +}); + +// ── Wait for main process to signal page load, then poll for settings window ── +ipcRenderer.on('main_did-finish-load', () => { + _console.log('[KCC] did-finish-load received, waiting to hook settings...'); + + const isGamePage = window.location.pathname === '/' || window.location.pathname === ''; + + // ── Batch all config reads into a single IPC call ── + (window as any).closeClient = () => window.close(); + Promise.all([ + ipcRenderer.invoke('get-all-config', ['ui', 'userscripts', 'game', 'translator', 'keybinds', 'discord']), + ipcRenderer.invoke('get-platform'), + ]).then(([allConf, platformInfo]: [any, any]) => { + const uiConf = allConf.ui; + const usConf = allConf.userscripts; + const gameConf = allConf.game; + const translatorConf = allConf.translator; + const discordConf = allConf.discord; + + // ── Exit button + UI toggles ── + const showExit = uiConf ? (uiConf.showExitButton !== false) : true; + const showExitBtn = () => { + const btn = document.getElementById('clientExit'); + if (btn) { + btn.style.display = showExit ? 'flex' : 'none'; + return true; + } + return false; + }; + if (!showExitBtn()) { + let exitAttempts = 0; + const exitPoll = setInterval(() => { + if (showExitBtn() || ++exitAttempts > 30) clearInterval(exitPoll); + }, 500); + } + + if (uiConf?.deathscreenAnimation) setDeathAnimBlock(true); + if (uiConf?.hideMenuPopups) startHidePopups(); + + // ── Initialize userscripts ── + const usEnabled = usConf ? usConf.enabled : true; + if (usEnabled) { + initUserscripts(_console).catch(err => _console.error('[KCC] Userscript init error:', err)); + } + + // ── Join as Spectator — auto-enable spectate on regular game join ── + if (isGamePage && gameConf?.joinAsSpectator) { + let attempts = 0; + const poll = setInterval(() => { + if (++attempts > 300) { clearInterval(poll); return; } + const uiBase = document.getElementById('uiBase'); + if (!uiBase || uiBase.className === '') return; + if (uiBase.className === 'onMenu') { + const specBtn = document.querySelector('#spectButton input') as HTMLInputElement; + if (specBtn && !specBtn.checked) { + (window as any).setSpect(1); + } + clearInterval(poll); + } else { + clearInterval(poll); + } + }, 100); + } + + // ── Initialize chat translator (game page only) ── + if (isGamePage) { + const mergedTl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf }; + initTranslator(_console, mergedTl); + } + + // ── Discord Rich Presence game state polling ── + if (isGamePage && discordConf?.enabled) { + let lastDetails = ''; + let lastState = ''; + let gameStartTimestamp = Math.floor(Date.now() / 1000); + + function pollDiscordState(): void { + let details = ''; + let state = ''; + let startTimestamp: number | undefined = undefined; + + const w = window as any; + const spectating = w.spectating; + + let gameActivity: any = null; + if (typeof w.getGameActivity === 'function') { + try { gameActivity = w.getGameActivity(); } catch {} + } + + if (spectating) { + details = 'Spectating'; + if (gameActivity?.map) { + state = gameActivity.map; + } + } else { + const uiBase = document.getElementById('uiBase'); + if (uiBase && uiBase.className === 'onMenu') { + details = 'In Menus'; + } else { + if (gameActivity?.mode && gameActivity?.map) { + details = gameActivity.mode + ' on ' + gameActivity.map; + } else { + const mapInfo = document.getElementById('mapInfo'); + details = mapInfo?.textContent || 'Playing Krunker'; + } + + if (gameActivity?.class?.name) { + state = gameActivity.class.name; + } else { + const classElem = document.getElementById('menuClassName'); + if (classElem?.textContent) state = classElem.textContent; + } + + startTimestamp = gameStartTimestamp; + } + } + + if (details !== lastDetails || state !== lastState) { + if (startTimestamp && lastDetails !== details) { + gameStartTimestamp = Math.floor(Date.now() / 1000); + startTimestamp = gameStartTimestamp; + } + lastDetails = details; + lastState = state; + ipcRenderer.send('discord-update', { + details, + state: state || undefined, + startTimestamp, + largeImageKey: 'krunker', + largeImageText: 'Krunker Civilian Client', + }); + } + } + + pollDiscordState(); + setInterval(pollDiscordState, 5000); + document.addEventListener('pointerlockchange', pollDiscordState); + } + // ── In-game Accounts quick-switch button ── + if (isGamePage) { + ipcRenderer.invoke('alt-list').then((accounts: any[]) => { + if (!accounts || accounts.length === 0) return; + + const altBtn = document.createElement('div'); + altBtn.id = 'kpcAltBtn'; + altBtn.className = 'menuItem'; + altBtn.setAttribute('onmouseenter', 'playTick()'); + altBtn.innerHTML = + 'people' + + ''; + + function showAltManager(): void { + const windowHolder = document.getElementById('windowHolder') as HTMLElement; + const menuWindow = document.getElementById('menuWindow') as HTMLElement; + const windowHeader = document.getElementById('windowHeader') as HTMLElement; + if (!windowHolder || !menuWindow || !windowHeader) return; + + if (windowHolder.style.display !== 'none' && windowHeader.innerText === 'Alt Manager') { + windowHolder.style.display = 'none'; + return; + } + + windowHolder.className = 'popupWin'; + windowHolder.style.display = 'block'; + menuWindow.classList.value = 'dark'; + menuWindow.style.cssText = 'width:800px;max-height:calc(100% - 330px);overflow-y:auto;top:50%;transform:translate(-50%,-50%);'; + windowHeader.innerText = 'Alt Manager'; + + function renderAccountList(): void { + ipcRenderer.invoke('alt-list').then((accs: any[]) => { + let html = + '
Alt Manager
' + + '
' + + '
Add Account
' + + '
'; + + if (!accs || accs.length === 0) { + html += '
No saved accounts
'; + } else { + accs.forEach((acc, i) => { + html += + '
' + + '' + escapeHtml(acc.label) + '' + + '' + + '
' + + '
' + + 'delete' + + '
' + + '
'; + }); + } + html += '
'; + menuWindow.innerHTML = html; + + const addBtn = document.getElementById('kpcAltAddBtn'); + if (addBtn) addBtn.addEventListener('click', showAddForm); + + menuWindow.querySelectorAll('.kpc-alt-login').forEach((el) => { + el.addEventListener('click', () => { + const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10); + if (accs[idx]) { + windowHolder.style.display = 'none'; + switchToAccount(accs[idx]); + } + }); + }); + + menuWindow.querySelectorAll('.kpc-alt-del').forEach((el) => { + el.addEventListener('click', () => { + const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10); + if (confirm('Delete account "' + (accs[idx]?.label || '') + '"?')) { + ipcRenderer.invoke('alt-remove', idx).then(() => renderAccountList()); + } + }); + }); + }); + } + + function showAddForm(): void { + menuWindow.innerHTML = + '
' + + '
Add Account
' + + '' + + '' + + '' + + '
' + + '
Add Account
' + + '
Back
' + + '
' + + '
'; + + document.getElementById('kpcAltBackBtn')!.addEventListener('click', renderAccountList); + document.getElementById('kpcAltSaveBtn')!.addEventListener('click', () => { + const label = (document.getElementById('kpcAltLabel') as HTMLInputElement).value.trim(); + const user = (document.getElementById('kpcAltUser') as HTMLInputElement).value.trim(); + const pass = (document.getElementById('kpcAltPass') as HTMLInputElement).value; + if (!label || !user || !pass) return; + ipcRenderer.invoke('alt-save', { + label, + username: encodeCredential(user), + password: encodeCredential(pass), + }).then(() => renderAccountList()); + }); + } + + renderAccountList(); + } + + altBtn.addEventListener('click', (e) => { + e.stopPropagation(); + (window as any).playSelect?.(); + showAltManager(); + }); + + function injectAltBtn(): boolean { + if (document.getElementById('kpcAltBtn')) return true; + const menuContainer = document.getElementById('menuItemContainer'); + if (!menuContainer) return false; + const exitBtn = document.getElementById('clientExit'); + if (exitBtn) { + menuContainer.insertBefore(altBtn, exitBtn); + } else { + menuContainer.appendChild(altBtn); + } + return true; + } + + if (!injectAltBtn()) { + let attempts = 0; + const poll = setInterval(() => { + if (injectAltBtn() || ++attempts > 60) clearInterval(poll); + }, 500); + } + }); + } + + }).catch(() => {}); + + const pollInterval = setInterval(() => { + const w = window as any; + if ( + hasOwn(w, 'showWindow') + && typeof w.showWindow === 'function' + && hasOwn(w, 'windows') + && Array.isArray(w.windows) + && w.windows.length >= 0 + && typeof w.windows[0] !== 'undefined' + && typeof w.windows[0].changeTab === 'function' + ) { + clearInterval(pollInterval); + _console.log('[KCC] Settings window found, hooking...'); + hookSettings(); + } + }, 500); +}); diff --git a/src/preload/matchmaker.ts b/src/preload/matchmaker.ts new file mode 100644 index 0000000..c3c96ed --- /dev/null +++ b/src/preload/matchmaker.ts @@ -0,0 +1,190 @@ +// ── Custom Matchmaker (ported from Crankshaft) ── +// Fetches live lobby list from matchmaker.krunker.io, filters by user criteria, +// presents a popup to join a random matching game. + +import type { Keybind } from '../main/config'; +import type { SavedConsole } from './utils'; + +export const MATCHMAKER_GAMEMODES = ['Free for All', 'Team Deathmatch', 'Hardpoint', 'Capture the Flag', 'Parkour', 'Hide & Seek', 'Infected', 'Race', 'Last Man Standing', 'Simon Says', 'Gun Game', 'Prop Hunt', 'Boss Hunt', 'Classic FFA', 'Deposit', 'Stalker', 'King of the Hill', 'One in the Chamber', 'Trade', 'Kill Confirmed', 'Defuse', 'Sharp Shooter', 'Traitor', 'Raid', 'Blitz', 'Domination', 'Squad Deathmatch', 'Kranked FFA', 'Team Defender', 'Deposit FFA', 'Chaos Snipers', 'Bighead FFA']; +export const MATCHMAKER_REGIONS = ['MBI', 'NY', 'FRA', 'SIN', 'DAL', 'SYD', 'MIA', 'BHN', 'TOK', 'BRZ', 'AFR', 'LON', 'CHI', 'SV', 'STL', 'MX']; +export const MATCHMAKER_REGION_NAMES: Record = { MBI: 'Mumbai', NY: 'New York', FRA: 'Frankfurt', SIN: 'Singapore', DAL: 'Dallas', SYD: 'Sydney', MIA: 'Miami', BHN: 'Middle East', TOK: 'Tokyo', BRZ: 'Brazil', AFR: 'South Africa', LON: 'London', CHI: 'China', SV: 'Silicon Valley', STL: 'Seattle', MX: 'Mexico' }; +const MAP_ICON_INDICES = ['Burg', 'Littletown', 'Sandstorm', 'Subzero', 'Undergrowth', 'Shipment', 'Freight', 'Lostworld', 'Citadel', 'Oasis', 'Kanji', 'Industry', 'Lumber', 'Evacuation', 'Site', 'SkyTemple', 'Lagoon', 'Bureau', 'Tortuga', 'Tropicano', 'Krunk_Plaza', 'Arena', 'Habitat', 'Atomic', 'Old_Burg', 'Throwback', 'Stockade', 'Facility', 'Clockwork', 'Laboratory', 'Shipyard', 'Soul Sanctum', 'Bazaar', 'Erupt', 'HQ', 'Khepri', 'Lush', 'Vivo', 'Slide Moonlight', 'Eterno Sim']; + +interface MatchmakerGame { + gameID: string; + region: string; + playerCount: number; + playerLimit: number; + map: string; + gamemode: string; + remainingTime: number; +} + +export interface MatchmakerConfig { + enabled: boolean; + regions: string[]; + gamemodes: string[]; + minPlayers: number; + maxPlayers: number; + minRemainingTime: number; + openServerBrowser: boolean; + acceptKey: Keybind; + cancelKey: Keybind; +} + +function secondsToTimestring(num: number): string { + const minutes = Math.floor(num / 60); + const seconds = num % 60; + if (minutes < 1) return `${num}s`; + return `${minutes}m ${seconds}s`; +} + +// ── Popup DOM (created once, reused) ── +const POPUP_ID = 'matchmakerPopupContainer'; +const popupElement = document.createElement('div'); +popupElement.id = POPUP_ID; + +const popupTitle = document.createElement('div'); +popupTitle.id = 'matchmakerPopupTitle'; +popupElement.appendChild(popupTitle); + +const popupDescription = document.createElement('div'); +popupDescription.id = 'matchmakerPopupDescription'; +popupElement.appendChild(popupDescription); + +const popupOptions = document.createElement('div'); +popupOptions.id = 'matchmakerPopupOptions'; + +const popupConfirmBtn = document.createElement('div'); +popupConfirmBtn.id = 'matchmakerConfirmButton'; +popupConfirmBtn.className = 'matchmakerPopupButton bigShadowT'; +popupConfirmBtn.textContent = 'Join'; +popupConfirmBtn.setAttribute('onmouseenter', 'playTick()'); +popupConfirmBtn.addEventListener('click', () => decideMatchmakerDecision(true)); + +const popupCancelBtn = document.createElement('div'); +popupCancelBtn.id = 'matchmakerCancelButton'; +popupCancelBtn.className = 'matchmakerPopupButton bigShadowT'; +popupCancelBtn.textContent = 'Cancel'; +popupCancelBtn.setAttribute('onmouseenter', 'playTick()'); +popupCancelBtn.addEventListener('click', () => decideMatchmakerDecision(false)); + +popupOptions.appendChild(popupConfirmBtn); +popupOptions.appendChild(popupCancelBtn); +popupElement.appendChild(popupOptions); + +// ── State ── +let currentMatch = ''; +let openServerBrowser = true; +let confirmKey: Keybind = { key: 'Enter', ctrl: false, shift: false, alt: false }; +let cancelKey: Keybind = { key: 'Escape', ctrl: false, shift: false, alt: false }; + +function decideMatchmakerDecision(accept: boolean): void { + const w = window as any; + if (typeof w.playSelect === 'function') w.playSelect(); + + if (accept && currentMatch !== 'none') { + window.location.href = `https://krunker.io/?game=${currentMatch}`; + } else { + if (popupElement.parentNode) popupElement.remove(); + if (currentMatch === 'none' && openServerBrowser && typeof w.openServerWindow === 'function') { + w.openServerWindow(0); + } + } +} + +function matchesKey(bind: Keybind, event: KeyboardEvent): boolean { + if ((document.activeElement as HTMLElement)?.tagName === 'INPUT') return false; + return event.key === bind.key + && event.shiftKey === bind.shift + && event.altKey === bind.alt + && event.ctrlKey === bind.ctrl; +} + +function handleMatchmakerBind(event: KeyboardEvent): void { + if (document.pointerLockElement) return; + const isAccept = matchesKey(confirmKey, event); + const isCancel = matchesKey(cancelKey, event); + if (isAccept || isCancel) { + document.removeEventListener('keydown', handleMatchmakerBind, true); + decideMatchmakerDecision(isAccept); + } +} + +function createFetchedGamePopup(game: MatchmakerGame): void { + const mapIdx = MAP_ICON_INDICES.indexOf(game.map); + popupElement.style.backgroundImage = `url(https://assets.krunker.io/img/maps/map_${mapIdx >= 0 ? mapIdx : 0}.png)`; + + currentMatch = game.gameID; + if (game.gameID === 'none') { + popupTitle.innerText = 'No Games Found...'; + popupDescription.innerHTML = 'Check the server browser to see other lobbies.'; + popupConfirmBtn.style.display = 'none'; + } else { + popupTitle.innerText = 'Game Found!'; + const regionName = MATCHMAKER_REGION_NAMES[game.region] ?? 'Unknown Region'; + popupDescription.innerHTML = `${game.gamemode} on ${game.map} (${regionName})
${game.playerCount}/${game.playerLimit} Players, ${secondsToTimestring(game.remainingTime)} Left`; + popupConfirmBtn.style.display = 'block'; + } + + document.addEventListener('keydown', handleMatchmakerBind, true); + const uiBase = document.getElementById('uiBase'); + if (uiBase) uiBase.appendChild(popupElement); +} + +export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole): Promise { + openServerBrowser = mmConfig.openServerBrowser; + confirmKey = mmConfig.acceptKey; + cancelKey = mmConfig.cancelKey; + + // Dismiss existing popup if active + if (document.getElementById(POPUP_ID)) decideMatchmakerDecision(false); + + _con?.log('[KCC-MM] Fetching game list...'); + + const response = await fetch(`https://matchmaker.krunker.io/game-list?hostname=${window.location.hostname}`); + const result = await response.json(); + const games: MatchmakerGame[] = []; + + for (const game of result.games) { + const gameID: string = game[0]; + const region = gameID.split(':')[0]; + const playerCount: number = game[2]; + const playerLimit: number = game[3]; + const map: string = game[4].i; + const gamemode = MATCHMAKER_GAMEMODES[game[4].g] ?? 'Unknown Gamemode'; + const remainingTime: number = game[5]; + + // Apply filters — empty arrays mean "all selected" (no filter) + if (mmConfig.regions.length > 0 && !mmConfig.regions.includes(region)) continue; + if (mmConfig.gamemodes.length > 0 && !mmConfig.gamemodes.includes(gamemode)) continue; + if (playerCount < mmConfig.minPlayers) continue; + if (playerCount > mmConfig.maxPlayers) continue; + if (remainingTime < mmConfig.minRemainingTime) continue; + if (playerCount === playerLimit) continue; + if (window.location.href.includes(gameID)) continue; + if (currentMatch === gameID) continue; + + games.push({ gameID, region, playerCount, playerLimit, map, gamemode, remainingTime }); + } + + _con?.log('[KCC-MM] Received', result.games?.length ?? 0, 'games,', games.length, 'passed filters'); + + if (games.length > 0) { + const selected = games[Math.floor(Math.random() * games.length)]; + _con?.log('[KCC-MM] Selected:', selected.gameID, selected.region, selected.map); + createFetchedGamePopup(selected); + } else { + _con?.log('[KCC-MM] No matching games found'); + createFetchedGamePopup({ + gameID: 'none', + region: 'none', + playerCount: 0, + playerLimit: 0, + map: MAP_ICON_INDICES[0], + gamemode: MATCHMAKER_GAMEMODES[0], + remainingTime: 0, + }); + } +} + diff --git a/src/preload/translator.ts b/src/preload/translator.ts new file mode 100644 index 0000000..f290cec --- /dev/null +++ b/src/preload/translator.ts @@ -0,0 +1,360 @@ +import type { SavedConsole } from './utils'; + +// ── Config ── + +interface TranslatorConfig { + enabled: boolean; + targetLanguage: string; + showLanguageTag: boolean; +} + +const DEFAULTS: TranslatorConfig = { + enabled: true, + targetLanguage: 'en', + showLanguageTag: true, +}; + +// ── Module state ── + +let _con: SavedConsole; +let cfg: TranslatorConfig = { ...DEFAULTS }; +let chatObserver: MutationObserver | null = null; +let pollTimer: ReturnType | null = null; + +// ── Translation cache (sessionStorage, 10-min expiry) ── + +const CACHE_KEY_PREFIX = 'kccTL_'; +const CACHE_EXPIRY_MS = 10 * 60 * 1000; + +interface CacheEntry { + t: string; // translation + l: string; // source language + ts: number; // timestamp +} + +function cacheGet(text: string): CacheEntry | null { + try { + const raw = sessionStorage.getItem(CACHE_KEY_PREFIX + text.toLowerCase().trim()); + if (!raw) return null; + const entry: CacheEntry = JSON.parse(raw); + if (Date.now() - entry.ts > CACHE_EXPIRY_MS) return null; + return entry; + } catch { return null; } +} + +function cacheSet(text: string, translation: string, srcLang: string): void { + try { + const entry: CacheEntry = { t: translation, l: srcLang, ts: Date.now() }; + sessionStorage.setItem(CACHE_KEY_PREFIX + text.toLowerCase().trim(), JSON.stringify(entry)); + } catch { /* sessionStorage full */ } +} + +// ── Skip terms (gaming/chat slang — never sent for translation) ── + +const SKIP_TERMS = new Set([ + // Greetings & basics + 'hi', 'hey', 'hello', 'yo', 'sup', 'bye', 'cya', 'gn', 'gm', + 'yes', 'no', 'yep', 'yea', 'yeah', 'nah', 'nope', 'ok', 'okay', 'kk', + // Chat abbreviations + 'lol', 'lmao', 'lmfao', 'rofl', 'omg', 'omfg', 'wtf', 'wth', + 'bruh', 'bro', 'dude', 'man', 'brb', 'afk', 'gtg', 'g2g', + 'smh', 'tbh', 'imo', 'imho', 'ngl', 'fr', 'frfr', 'fax', + 'idk', 'idc', 'idgaf', 'nvm', 'stfu', 'pls', 'plz', + 'thx', 'ty', 'tysm', 'np', 'yw', 'mb', 'sry', 'sorry', + 'bet', 'cap', 'nocap', 'sus', 'mid', 'based', 'cringe', 'ratio', + 'rip', 'oof', 'uwu', 'owo', 'xd', 'xdd', 'xddd', 'lel', 'kek', + 'damn', 'dang', 'boi', 'fam', 'goat', 'goated', + 'lit', 'vibe', 'vibes', 'lowkey', 'highkey', 'deadass', + 'nice', 'cool', 'sick', 'fire', 'trash', 'ass', 'toxic', + 'wow', 'whoa', 'wha', 'huh', 'wat', 'wut', 'hmm', + // Gaming general + 'gg', 'ggwp', 'ggez', 'wp', 'ez', 'gl', 'hf', 'glhf', + 'nt', 'ns', 'gj', 'mvp', 'clutch', 'ace', 'carry', + 'noob', 'newb', 'n00b', 'bot', 'tryhard', 'sweat', 'sweaty', + 'hack', 'hacks', 'hacker', 'hax', 'cheater', 'cheats', + 'lag', 'laggy', 'ping', 'fps', 'dc', 'disconnect', + 'nerf', 'buff', 'op', 'broken', 'meta', 'spam', 'camp', 'camper', + 'aim', 'aimbot', 'wh', 'wallhack', 'esp', + 'rush', 'push', 'rotate', 'flank', 'peek', 'hold', + 'one', 'low', 'dead', 'down', 'res', 'revive', + 'w', 'l', 'dub', 'win', 'loss', 'f', 'ggs', + // Krunker-specific + 'kr', 'ak', 'smg', 'sniper', 'shotty', 'rev', 'semi', + 'crossy', 'famas', 'rpg', 'lmg', 'deagle', 'comp', + 'pub', 'pubs', 'ranked', 'nuke', 'nuked', 'nuking', + 'kpd', 'bhop', 'bhopping', 'slidehopping', 'slidehop', + 'krunker', 'krunky', 'yendis', 'krunkitis', + 'contra', 'relic', 'unob', 'unobtainable', 'spin', + 'market', 'trade', 'gift', 'drop', 'drops', 'skin', 'skins', + 'clan', 'verified', 'lvl', 'level', + 'trig', 'trigger', 'runner', 'det', 'detective', + 'vince', 'bowman', 'spray', 'agent', 'rocketeer', + 'streamer', 'ttv', + // Emoticons + ':)', ':(', ':d', ':p', ':o', '<3', +]); + +// ── False-positive source languages ── + +const FALSE_POSITIVE_LANGS = new Set([ + 'so', 'cy', 'ht', 'hmn', 'ceb', 'haw', 'la', 'mg', 'mi', + 'ny', 'sm', 'st', 'su', 'sw', 'tl', 'yo', 'zu', 'sn', + 'ig', 'rw', 'co', 'fy', 'gd', 'lb', 'mt', 'eo', +]); + +// ── Auto-suppression (repeated short phrases) ── + +const suppressionCounts = new Map(); +const SUPPRESS_THRESHOLD = 3; +const MIN_LATIN_WORDS = 3; +const SHORT_TEXT_THRESHOLD = 15; + +// ── Concurrency control ── + +let activeRequests = 0; +const MAX_CONCURRENT = 3; +const pendingQueue: Array<() => void> = []; + +function enqueue(fn: () => Promise): void { + if (activeRequests < MAX_CONCURRENT) { + activeRequests++; + fn().finally(() => { + activeRequests--; + if (pendingQueue.length > 0) pendingQueue.shift()!(); + }); + } else { + pendingQueue.push(() => enqueue(fn)); + } +} + +// ── System message patterns to skip ── + +const SYSTEM_PATTERNS = [ + 'joined the game', 'left the game', 'has been kicked', 'has been banned', + 'vote to kick', 'press f1', 'connecting', 'connected', 'was arrested', + 'started a vote', 'was kicked', 'was banned', +]; + +// ── Pre-translation filtering ── + +function isLatinOnly(text: string): boolean { + return /^[\x00-\x7F\u00C0-\u024F\u1E00-\u1EFF\s\d.,!?;:'"()\-/@#$%^&*+=~`[\]{}|\\<>]+$/u.test(text); +} + +function shouldTranslate(text: string): boolean { + const cleaned = text.trim(); + if (cleaned.length < 2) return false; + + // Tokenize for skip-term checking + const words = cleaned.replace(/[^a-zA-Z0-9\s]/g, '').toLowerCase().split(/\s+/).filter(w => w.length > 0); + if (words.length === 0) return false; + if (words.every(w => SKIP_TERMS.has(w))) return false; + + // Auto-suppressed phrases + const key = cleaned.toLowerCase(); + if ((suppressionCounts.get(key) ?? 0) >= SUPPRESS_THRESHOLD) return false; + + // Non-Latin characters = almost certainly needs translation + if (!isLatinOnly(cleaned)) return true; + + // Latin-only: require minimum word count (short English slang triggers false positives) + if (words.length < MIN_LATIN_WORDS) { + // Allow if accented characters suggest non-English + if (!/[À-ÿ]/.test(cleaned)) return false; + } + + return true; +} + +// ── Chat text extraction ── + +interface ChatExtraction { + message: string; + username: string; // "Username:" prefix or empty +} + +function extractChatText(node: HTMLElement): ChatExtraction | null { + const text = node.textContent?.trim(); + if (!text || text.length < 2) return null; + + // Skip nodes with images (kill feed has weapon/skull icons) + if (node.querySelector('img')) return null; + + // Skip commands + if (text.startsWith('/')) return null; + + // Skip system messages + const lower = text.toLowerCase(); + if (SYSTEM_PATTERNS.some(p => lower.includes(p))) return null; + + // Extract message content after "Username: " prefix + const colonIdx = text.indexOf(':'); + if (colonIdx > 0 && colonIdx < 25) { + const username = text.substring(0, colonIdx + 1); + const msg = text.substring(colonIdx + 1).trim(); + return msg.length >= 2 ? { message: msg, username } : null; + } + + return { message: text, username: '' }; +} + +// ── Google Translate API ── + +async function translateText(text: string): Promise<{ translation: string; srcLang: string } | null> { + // Check cache + const cached = cacheGet(text); + if (cached) return { translation: cached.t, srcLang: cached.l }; + + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + const url = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=' + + cfg.targetLanguage + '&dt=t&q=' + encodeURIComponent(text); + + const response = await fetch(url, { signal: controller.signal }); + clearTimeout(timeout); + + if (!response.ok) { + _con.warn('[KCC-TL] HTTP', response.status); + return null; + } + + const data = await response.json(); + if (!data?.[0]?.[0]) return null; + + const translation = (data[0] as any[]).map((item: any) => item[0]).join(''); + const srcLang: string = data[2] || 'unknown'; + + // Already in target language + if (srcLang === cfg.targetLanguage) return null; + + // Identical translation (strip punctuation/whitespace for robust comparison) + const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); + if (norm(translation) === norm(text)) return null; + + // Post-filter: false-positive languages on short text + if (text.length < SHORT_TEXT_THRESHOLD && FALSE_POSITIVE_LANGS.has(srcLang)) { + const key = text.toLowerCase().trim(); + suppressionCounts.set(key, (suppressionCounts.get(key) ?? 0) + 1); + return null; + } + + // Track short phrases for auto-suppression learning + const wordCount = text.trim().split(/\s+/).length; + if (wordCount <= 2) { + const key = text.toLowerCase().trim(); + const count = (suppressionCounts.get(key) ?? 0) + 1; + suppressionCounts.set(key, count); + if (count >= SUPPRESS_THRESHOLD) return null; + } + + cacheSet(text, translation, srcLang); + return { translation, srcLang }; + } catch (err: any) { + if (err.name !== 'AbortError') _con.warn('[KCC-TL] Error:', err.message); + return null; + } +} + +// ── DOM manipulation ── + +function appendTranslation(chatNode: HTMLElement, username: string, translation: string, srcLang: string): void { + const div = document.createElement('div'); + div.className = 'kcc-translation'; + + const langTag = (cfg.showLanguageTag && srcLang !== 'unknown') ? ' [' + srcLang.toUpperCase() + ']' : ''; + div.textContent = '\u{1F310} ' + (username ? username + ' ' : '') + translation + langTag; + chatNode.appendChild(div); +} + +// ── Message processing ── + +function processMessage(node: HTMLElement): void { + if (node.hasAttribute('data-kpc-translated')) return; + node.setAttribute('data-kpc-translated', '1'); + + const extracted = extractChatText(node); + if (!extracted) return; + if (!shouldTranslate(extracted.message)) return; + + const { message, username } = extracted; + enqueue(async () => { + const result = await translateText(message); + if (result) appendTranslation(node, username, result.translation, result.srcLang); + }); +} + +// ── Observer lifecycle ── + +function startObserver(): void { + if (chatObserver) return; + + let attempts = 0; + pollTimer = setInterval(() => { + attempts++; + const chatList = document.getElementById('chatList'); + if (!chatList) { + if (attempts > 60) { + clearInterval(pollTimer!); + pollTimer = null; + _con.warn('[KCC-TL] #chatList not found after 30s, giving up'); + } + return; + } + + clearInterval(pollTimer!); + pollTimer = null; + + chatObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.nodeType === 1) processMessage(node as HTMLElement); + } + } + }); + + chatObserver.observe(chatList, { childList: true }); + _con.log('[KCC-TL] Chat observer active'); + }, 500); +} + +function stopObserver(): void { + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + if (chatObserver) { + chatObserver.disconnect(); + chatObserver = null; + } +} + +// ── Public API ── + +export function initTranslator(savedConsole: SavedConsole, initCfg: TranslatorConfig): void { + _con = savedConsole; + cfg = { + enabled: initCfg.enabled ?? DEFAULTS.enabled, + targetLanguage: initCfg.targetLanguage ?? DEFAULTS.targetLanguage, + showLanguageTag: initCfg.showLanguageTag ?? DEFAULTS.showLanguageTag, + }; + + if (!cfg.enabled) { + _con.log('[KCC-TL] Translator disabled'); + return; + } + + _con.log('[KCC-TL] Initializing (target: ' + cfg.targetLanguage + ')'); + startObserver(); +} + +export function updateTranslatorConfig(update: Partial): void { + if (update.enabled !== undefined) { + cfg.enabled = update.enabled; + if (update.enabled && !chatObserver) startObserver(); + if (!update.enabled) stopObserver(); + } + if (update.targetLanguage !== undefined) cfg.targetLanguage = update.targetLanguage; + if (update.showLanguageTag !== undefined) cfg.showLanguageTag = update.showLanguageTag; +} diff --git a/src/preload/userscripts.ts b/src/preload/userscripts.ts new file mode 100644 index 0000000..2c01548 --- /dev/null +++ b/src/preload/userscripts.ts @@ -0,0 +1,258 @@ +import { ipcRenderer, webFrame } from 'electron'; + +// ── Types ── + +export interface ScriptMetadata { + name: string; + author: string; + version: string; + desc: string; + src: string; + license: string; + runAt: 'document-start' | 'document-end'; + priority: number; +} + +export interface UserscriptSetting { + title: string; + type: 'bool' | 'num' | 'sel' | 'color' | 'keybind'; + value: unknown; + desc?: string; + min?: number; + max?: number; + step?: number; + opts?: (string | number)[]; + changed?: (value: unknown) => void; +} + +export interface UserscriptInstance { + filename: string; + content: string; + meta: ScriptMetadata; + enabled: boolean; + executed: boolean; + unload: (() => void) | null; + settings: Record | null; +} + +// ── State ── + +const instances: UserscriptInstance[] = []; +const cssHandles = new Map(); // identifier -> webFrame CSS key + +// ── Metadata parser ── + +export function parseMetadata(code: string): ScriptMetadata { + const meta: ScriptMetadata = { + name: '', + author: '', + version: '', + desc: '', + src: '', + license: '', + runAt: 'document-end', + priority: 0, + }; + + const startMatch = code.match(/\/\/\s*==UserScript==/); + const endMatch = code.match(/\/\/\s*==\/UserScript==/); + if (!startMatch || !endMatch) return meta; + + const block = code.substring( + startMatch.index! + startMatch[0].length, + endMatch.index!, + ); + + for (const line of block.split('\n')) { + const m = line.match(/\/\/\s*@(\S+)\s+(.*)/); + if (!m) continue; + const [, tag, val] = m; + const v = val.trim(); + switch (tag) { + case 'name': meta.name = v; break; + case 'author': meta.author = v; break; + case 'version': meta.version = v; break; + case 'desc': + case 'description': meta.desc = v; break; + case 'src': meta.src = v; break; + case 'license': meta.license = v; break; + case 'run-at': + if (v === 'document-start') meta.runAt = 'document-start'; + else meta.runAt = 'document-end'; + break; + case 'priority': + meta.priority = parseInt(v, 10) || 0; + break; + } + } + + return meta; +} + +// ── CSS injection via webFrame ── + +function toggleCSS(css: string, identifier: string, value: boolean): void { + const existing = cssHandles.get(identifier); + if (value) { + if (existing) return; // already inserted + const key = webFrame.insertCSS(css); + cssHandles.set(identifier, key); + } else { + if (!existing) return; + webFrame.removeInsertedCSS(existing); + cssHandles.delete(identifier); + } +} + +// ── Script execution ── + +function executeScript( + instance: UserscriptInstance, + _console: { log: Function; warn: Function; error: Function }, +): void { + if (instance.executed) return; + + const context: Record = { + _console, + _css(css: string, identifier: string, value: boolean) { + toggleCSS(css, instance.filename + ':' + identifier, value); + }, + unload: null as (() => void) | null, + settings: null as Record | null, + }; + + try { + const fn = new Function(instance.content); + const result = fn.apply(context); + + // Script returned `this` — capture settings and unload + if (result === context) { + instance.unload = (typeof context.unload === 'function') ? context.unload as () => void : null; + instance.settings = context.settings as Record | null; + } else { + instance.unload = null; + instance.settings = null; + } + + instance.executed = true; + _console.log('[KCC] Userscript executed:', instance.meta.name || instance.filename); + } catch (err) { + _console.error('[KCC] Userscript error in', instance.filename, ':', err); + } +} + +// ── Apply saved preferences ── + +async function applyPreferences(instance: UserscriptInstance): Promise { + if (!instance.settings) return; + const saved = await ipcRenderer.invoke('userscripts-load-prefs', instance.filename); + for (const key of Object.keys(instance.settings)) { + if (key in saved) { + const setting = instance.settings[key]; + setting.value = saved[key]; + if (typeof setting.changed === 'function') { + try { setting.changed(setting.value); } catch { /* ignore callback errors */ } + } + } + } +} + +// ── Public API ── + +export function getInstances(): UserscriptInstance[] { + return instances; +} + +export async function initUserscripts( + _console: { log: Function; warn: Function; error: Function }, +): Promise { + const { scripts, tracker } = await ipcRenderer.invoke('userscripts-scan'); + if (!scripts || scripts.length === 0) { + _console.log('[KCC] No userscripts found'); + return; + } + + // Build instances + for (const script of scripts) { + const meta = parseMetadata(script.content); + instances.push({ + filename: script.filename, + content: script.content, + meta, + enabled: tracker[script.filename] === true, + executed: false, + unload: null, + settings: null, + }); + } + + // Sort by priority descending + instances.sort((a, b) => b.meta.priority - a.meta.priority); + + // Execute document-start scripts + for (const inst of instances) { + if (inst.enabled && inst.meta.runAt === 'document-start') { + executeScript(inst, _console); + await applyPreferences(inst); + } + } + + // Execute document-end scripts + const runDocEnd = () => { + for (const inst of instances) { + if (inst.enabled && inst.meta.runAt === 'document-end' && !inst.executed) { + executeScript(inst, _console); + applyPreferences(inst); + } + } + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', runDocEnd, { once: true }); + } else { + runDocEnd(); + } + + _console.log('[KCC] Userscripts initialized:', instances.length, 'scripts loaded'); +} + +export function setScriptEnabled( + filename: string, + enabled: boolean, + _console: { log: Function; warn: Function; error: Function }, +): { needsReload: boolean } { + const inst = instances.find(i => i.filename === filename); + if (!inst) return { needsReload: false }; + + inst.enabled = enabled; + + // Update tracker + const tracker: Record = {}; + for (const i of instances) tracker[i.filename] = i.enabled; + ipcRenderer.invoke('userscripts-set-tracker', tracker); + + if (!enabled) { + if (inst.unload && inst.executed) { + try { + inst.unload(); + _console.log('[KCC] Userscript unloaded:', inst.meta.name || inst.filename); + } catch (err) { + _console.error('[KCC] Userscript unload error:', err); + } + inst.executed = false; + inst.unload = null; + inst.settings = null; + return { needsReload: false }; + } + // No unload function — need page reload to fully disable + return { needsReload: inst.executed }; + } else { + // Enabling + if (!inst.executed) { + executeScript(inst, _console); + applyPreferences(inst); + return { needsReload: false }; + } + return { needsReload: false }; + } +} diff --git a/src/preload/utils.ts b/src/preload/utils.ts new file mode 100644 index 0000000..865fbe3 --- /dev/null +++ b/src/preload/utils.ts @@ -0,0 +1,75 @@ +// ── Shared preload utilities ── +// Common types, helpers, and constants used across preload modules. + +// ── Shared interfaces ── + +export interface SavedConsole { + log: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} + +export interface KeybindDef { + key: string; + ctrl: boolean; + shift: boolean; + alt: boolean; +} + +// ── HTML escaping ── + +const HTML_ESCAPE_MAP: Record = { + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', +}; + +export function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c]); +} + +// ── Chat message injection ── +// Creates messages in #chatHolder inside a persistent #kpcMessageHolder div. +// timeout=0 means the message is persistent (not auto-removed). + +export function genChatMsg(text: string, timeout = 2.25): HTMLElement | null { + const chatHolder = document.getElementById('chatHolder'); + if (!chatHolder) return null; + if (!document.getElementById('kpcMessageHolder')) { + chatHolder.insertAdjacentHTML('afterbegin', '
'); + } + const holder = document.getElementById('kpcMessageHolder')!; + holder.insertAdjacentHTML('beforeend', + '
' + + escapeHtml(text) + '
'); + const elem = holder.lastElementChild as HTMLElement; + if (timeout !== 0) { + setTimeout(() => { elem.remove(); }, timeout * 1000); + } + return elem; +} + +// ── Filename sanitisation ── + +export function sanitizeFilename(name: string): string { + return name.replace(/[^a-zA-Z0-9_-]/g, '_'); +} + +// ── Shared CSS constants ── + +export const DEATH_ANIM_BLOCK_ID = 'kpc-animationBlock'; +export const DEATH_ANIM_BLOCK_CSS = + '.death-ui-bottom, .death-ui-bottom-empty { animation: none !important; transition: none !important; }'; + +/** Inject or remove the death screen animation block style element. */ +export function setDeathAnimBlock(enabled: boolean): void { + let el = document.getElementById(DEATH_ANIM_BLOCK_ID); + if (enabled) { + if (!el) { + el = document.createElement('style'); + el.id = DEATH_ANIM_BLOCK_ID; + el.textContent = DEATH_ANIM_BLOCK_CSS; + document.head.appendChild(el); + } + } else if (el) { + el.remove(); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e63b4a8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "out"] +} diff --git a/vite.main.config.ts b/vite.main.config.ts new file mode 100644 index 0000000..63244f9 --- /dev/null +++ b/vite.main.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import { builtinModules } from 'module'; + +// Both 'fs' and 'node:fs' forms must be externalized +const nodeBuiltins = builtinModules.flatMap((m) => [m, `node:${m}`]); + +const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--mode'); + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/main/index.ts'), + formats: ['cjs'], + fileName: () => 'index.js', + }, + outDir: 'dist/main', + emptyDirBefore: true, + rollupOptions: { + external: ['electron', 'electron-store', ...nodeBuiltins], + }, + target: 'node20', + minify: isProd, + sourcemap: !isProd, + }, + resolve: { + // Treat this as a Node build — don't swap node builtins for browser stubs + conditions: ['node'], + mainFields: ['module', 'main'], + }, +}); diff --git a/vite.preload.config.ts b/vite.preload.config.ts new file mode 100644 index 0000000..9aed58e --- /dev/null +++ b/vite.preload.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--mode'); + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/preload/index.ts'), + formats: ['cjs'], + fileName: () => 'index.js', + }, + outDir: 'dist/preload', + emptyDirBefore: true, + rollupOptions: { + external: ['electron', 'uuid'], + }, + target: 'node20', + minify: isProd, + sourcemap: !isProd, + }, +});