diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index c793582c..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "env": {
- "node": true,
- "browser": true
- },
- "plugins": [
- "svelte3"
- ],
- "overrides": [
- {
- "files": ["*.svelte"],
- "processor": "svelte3/svelte3"
- }
- ],
- "parserOptions": {
- "ecmaVersion": 2019,
- "sourceType": "module"
- },
- "rules": {
- "accessor-pairs": "error",
- "block-spacing": ["error", "never"],
- "brace-style": ["error", "stroustrup", {"allowSingleLine": true}],
- "curly": ["error", "multi-line", "consistent"],
- "dot-location": ["error", "property"],
- "dot-notation": "error",
- // "eqeqeq": ["error", "smart"],
- "func-call-spacing": "error",
- "handle-callback-err": "error",
- "key-spacing": "error",
- "keyword-spacing": "error",
- "linebreak-style": ["error", "windows"],
- "new-cap": ["error", {"newIsCap": true}],
- "no-array-constructor": "error",
- "no-caller": "error",
- "no-console": "error",
- "no-duplicate-imports": "error",
- "no-else-return": "error",
- "no-eval": "error",
- "no-floating-decimal": "error",
- "no-implied-eval": "error",
- "no-iterator": "error",
- "no-label-var": "error",
- "no-labels": "error",
- "no-lone-blocks": "error",
- "no-mixed-spaces-and-tabs": "error",
- "no-multi-spaces": "error",
- "no-multi-str": "error",
- "no-new": "error",
- "no-new-func": "error",
- "no-new-object": "error",
- "no-new-wrappers": "error",
- "no-octal-escape": "error",
- "no-path-concat": "error",
- "no-proto": "error",
- "no-prototype-builtins": "off",
- "no-redeclare": ["error", {"builtinGlobals": true}],
- "no-self-compare": "error",
- "no-sequences": "error",
- "no-shadow": ["warn", {"builtinGlobals": false, "hoist": "functions"}],
- "no-tabs": "error",
- "no-template-curly-in-string": "error",
- "no-throw-literal": "error",
- // "no-trailing-spaces": "error",
- "no-undef": "error",
- "no-undef-init": "error",
- "no-unmodified-loop-condition": "error",
- "no-unneeded-ternary": "error",
- "no-useless-call": "error",
- "no-useless-computed-key": "error",
- "no-useless-constructor": "error",
- "no-useless-rename": "error",
- "no-var": "error",
- "no-whitespace-before-property": "error",
- "object-curly-spacing": ["error", "never", {"objectsInObjects": false}],
- "object-property-newline": ["error", {"allowAllPropertiesOnSameLine": true}],
- "operator-linebreak": ["error", "none", {"overrides": {"?": "before", ":": "before"}}],
- "prefer-const": "error",
- "quote-props": ["error", "consistent-as-needed", {"keywords": true}],
- "quotes": ["error", "double", {"allowTemplateLiterals": true}],
- "rest-spread-spacing": "error",
- "semi": "error",
- "semi-spacing": "error",
- "space-before-blocks": "error",
- "space-in-parens": "error",
- "space-infix-ops": "error",
- "space-unary-ops": ["error", {"words": true, "nonwords": false, "overrides": {"typeof": false}}],
- "spaced-comment": ["error", "always", {"exceptions": ["-", "*"]}],
- "template-curly-spacing": "error",
- "wrap-iife": ["error", "inside"],
- "yield-star-spacing": "error",
- "yoda": "error"
- },
- "globals": {
- "Proxy": "readonly",
- "Set": "readonly",
- "WeakMap": "readonly",
- "Map": "readonly",
- "Promise": "readonly",
- "Reflect": "readonly",
- "DiscordNative": "readonly",
- "__non_webpack_require__": "readonly",
- "Symbol": "readonly",
- "__static": "readonly",
- "status": "off"
- }
-}
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 04acc8c3..cf50b2fa 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-github: rauenzi
+github: zerebos
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..1efb8266
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,84 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ frontend:
+ name: Frontend Checks
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+
+ - name: Install dependencies
+ run: bun install
+ working-directory: frontend
+
+ - name: Typecheck
+ run: bun run check
+ working-directory: frontend
+
+ - name: Lint
+ run: bun run lint
+ working-directory: frontend
+
+ - name: Build
+ run: bun run build
+ working-directory: frontend
+
+ backend:
+ name: Go Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Run tests
+ run: go test ./...
+
+ build:
+ name: Wails Build
+ runs-on: ubuntu-latest
+ needs:
+ - frontend
+ - backend
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install Linux dependencies
+ if: ${{ runner.os == 'Linux' }}
+ run: sudo apt-get -yq update && sudo apt-get -yq install libgtk-3-0 libwebkit2gtk-4.1-dev gcc-aarch64-linux-gnu
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+
+ - name: Install Wails
+ run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
+
+ - name: Build
+ run: wails build -tags webkit2_41
diff --git a/.github/workflows/release-pipeline.yml b/.github/workflows/release-pipeline.yml
deleted file mode 100644
index 55c17461..00000000
--- a/.github/workflows/release-pipeline.yml
+++ /dev/null
@@ -1,190 +0,0 @@
-name: Release Pipeline
-
-# Execution needs to be triggered manually at https://github.com/BetterDiscord/Installer/actions/workflows/release-pipeline.yml.
-
-# This pipeline is pretty hard-coded and will deliver
-# identical results no matter the branch it's run against.
-
-on:
- workflow_dispatch:
- inputs:
- version_tag:
- description: The version label to be used for this release
- required: true
-
-concurrency: release
-
-jobs:
-
- # Checkout 'release', merge 'development', bump version, upload source artifact.
- prepare:
- name: Prepare Repo
- runs-on: ubuntu-latest
- outputs:
- old_version: ${{ steps.version_bump.outputs.old_version }}
- new_version: ${{ steps.version_bump.outputs.new_version }}
- steps:
-
- - name: checkout 'release'
- uses: actions/checkout@v2
- with:
- ref: 'release'
- fetch-depth: 0
-
- - name: merge 'development' and bump version
- id: version_bump
- run: |
- git config --global user.name "BetterDiscord CI"
- git config --global user.email "BetterDiscord@users.noreply.github.com"
- git merge --no-ff --no-commit 'origin/development'
- node << 'EOF'
- const fs = require("fs");
- const packageJson = JSON.parse(fs.readFileSync("package.json", "utf-8"));
- let version = process.env.VERSION_TAG;
- if (/^v\d/.test(version)) version = version.slice(1);
- if (
- !/^\d/.test(version) || version.includes("-") && !/[a-z\d]$/.test(version)
- ) throw new Error(`Bad version tag: '${process.env.VERSION_TAG}'`);
- if (version.includes("-") && !/\.\d+$/.test(version)) version += ".0";
- version = version.replace(/\.+/g, ".");
- fs.writeFileSync("package.json", JSON.stringify({
- ...packageJson,
- version,
- }, null, 2) + "\n");
- let oldVersion = "v" + packageJson.version;
- let newVersion = "v" + version;
- console.log(`::set-output name=old_version::${oldVersion}`);
- console.log(`::set-output name=new_version::${newVersion}`);
- fs.writeFileSync("commit-message", `CI: Prepare release '${newVersion}`);
- EOF
- git add package.json
- git commit -F commit-message
- env:
- VERSION_TAG: ${{ github.event.inputs.version_tag }}
-
- - uses: actions/upload-artifact@v2
- with:
- name: source
- path: |
- ./*
- !.git/config
-
- # Download source artifact, build, upload build artifact.
- # Runs once on each release platform.
- build:
- name: Build
- needs: prepare
- strategy:
- fail-fast: true
- matrix:
- os:
- # ordered by how fast they build (muh cosmetics)
- - ubuntu-latest
- - windows-latest
- - macos-latest
- runs-on: ${{ matrix.os }}
- steps:
-
- - uses: actions/download-artifact@v2
- with:
- name: source
-
- - run: yarn install && yarn dist
- env:
- CSC_LINK: ${{ secrets.CSC_LINK }}
- CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
-
- - uses: actions/upload-artifact@v2
- if: ${{ success() && matrix.os == 'ubuntu-latest' }}
- with:
- name: build
- path: dist/BetterDiscord-Linux.AppImage
- if-no-files-found: error
-
- - uses: actions/upload-artifact@v2
- if: ${{ success() && matrix.os == 'windows-latest' }}
- with:
- name: build
- path: dist/BetterDiscord-Windows.exe
- if-no-files-found: error
-
- - if: ${{ success() && matrix.os == 'macos-latest' }}
- run: mv -f dist/BetterDiscord-*-mac.zip dist/BetterDiscord-Mac.zip
- - if: ${{ success() && matrix.os == 'macos-latest' }}
- uses: actions/upload-artifact@v2
- with:
- name: build
- path: dist/BetterDiscord-Mac.zip
- if-no-files-found: error
-
- # Download source artifact, push to 'release'.
- push_changes:
- name: Push Version Bump
- needs:
- - prepare
- - build
- runs-on: ubuntu-latest
- outputs:
- sha_1: ${{ steps.push.outputs.sha_1 }}
- steps:
-
- - name: checkout 'development'
- uses: actions/checkout@v2
- with:
- ref: 'development'
- token: ${{ secrets.CI_PAT }}
- clean: false
- - uses: actions/download-artifact@v2
- with:
- name: source
-
- - name: merge 'release' and push everything
- id: push
- run: |
- git checkout 'development'
- git config --global user.name "BetterDiscord CI"
- git config --global user.email "BetterDiscord@users.noreply.github.com"
- # TODO: rebase 'development' on 'release'
- git merge --ff-only --no-commit 'release'
- SHA1=$(git rev-parse --verify HEAD)
- git tag "$NEW_VERSION" "$SHA1"
- git push --all && git push --tags
- echo "::set-output name=sha_1::$SHA1"
- env:
- NEW_VERSION: ${{ needs.prepare.outputs.new_version }}
-
- # Download build artifact, do github release.
- publish:
- name: Draft Release
- needs:
- - prepare
- - build
- - push_changes
- runs-on: ubuntu-latest
- steps:
-
- - uses: actions/checkout@v2
- with:
- ref: ${{ needs.push_changes.outputs.sha_1 }}
-
- - name: authenticate gh-cli
- run: echo "$GITHUB_TOKEN_STUPIDO" | gh auth login --with-token
- env:
- GITHUB_TOKEN_STUPIDO: ${{ secrets.CI_PAT }}
-
- - uses: actions/download-artifact@v2
- with:
- name: build
-
- - name: upload release draft
- run: |
- gh release create "$NEW_VERSION" --draft --title "$NEW_VERSION" --target "$SHA1" --generate-notes \
- "BetterDiscord-Linux.AppImage#Linux (AppImage)" \
- "BetterDiscord-Mac.zip#Mac OS (Zip)" \
- "BetterDiscord-Windows.exe#Windows (Exe)"
- env:
- SHA1: ${{ needs.push_changes.outputs.sha_1 }}
- OLD_VERSION: ${{ needs.prepare.outputs.old_version }}
- NEW_VERSION: ${{ needs.prepare.outputs.new_version }}
- NAKED_INPUT_VERSION_TAG: ${{ github.event.inputs.version_tag }}
- RELEASE_NOTES_TEMPLATE_LOCATION: .github/RELEASE_TEMPLATE.md
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..47ece9d6
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,132 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*"
+
+permissions:
+ contents: write
+
+concurrency:
+ group: release-${{ github.ref_name }}
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install Linux dependencies
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ run: sudo apt-get -yq update && sudo apt-get -yq install libgtk-3-0 libwebkit2gtk-4.1-dev gcc-aarch64-linux-gnu
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+
+ - name: Install Wails
+ run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
+
+ - name: Derive version
+ id: version
+ shell: bash
+ run: echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
+
+ - name: Sync Wails product version
+ shell: bash
+ run: |
+ bun << 'EOF'
+ const fs = require("fs");
+ const version = process.env.RELEASE_VERSION.replace(/^v/, "");
+ const wailsConfig = JSON.parse(fs.readFileSync("wails.json", "utf-8"));
+ wailsConfig.info.productVersion = version;
+ fs.writeFileSync("wails.json", JSON.stringify(wailsConfig, null, 2) + "\n");
+ EOF
+ env:
+ RELEASE_VERSION: ${{ steps.version.outputs.version }}
+
+ - name: Build installer (Linux)
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ shell: bash
+ run: wails build -tags webkit2_41 -ldflags="-X main.version=${{ steps.version.outputs.version }}"
+
+ - name: Build installer (Windows)
+ if: ${{ matrix.os == 'windows-latest' }}
+ shell: bash
+ run: wails build -webview2 embed -ldflags="-X main.version=${{ steps.version.outputs.version }}"
+
+ - name: Build installer (macOS)
+ if: ${{ matrix.os == 'macos-latest' }}
+ shell: bash
+ run: wails build -ldflags="-X main.version=${{ steps.version.outputs.version }}"
+
+ - name: Upload Linux binary
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-linux
+ path: build/bin/BetterDiscord-Installer
+ if-no-files-found: error
+
+ - name: Upload Windows binary
+ if: ${{ matrix.os == 'windows-latest' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-windows
+ path: build/bin/BetterDiscord-Installer.exe
+ if-no-files-found: error
+
+ - name: Upload macOS app
+ if: ${{ matrix.os == 'macos-latest' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-macos
+ path: build/bin/BetterDiscord-Installer.app
+ if-no-files-found: error
+
+ release:
+ name: Create Release
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Download Linux artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: build-linux
+ path: release
+
+ - name: Download macOS artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: build-macos
+ path: release
+
+ - name: Download Windows artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: build-windows
+ path: release
+
+ - name: Create GitHub release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh release create "${GITHUB_REF_NAME}" --draft --title "${GITHUB_REF_NAME}" --generate-notes \
+ "release/BetterDiscord-Installer#Linux" \
+ "release/BetterDiscord-Installer.app#macOS" \
+ "release/BetterDiscord-Installer.exe#Windows"
diff --git a/.gitignore b/.gitignore
index 72623cce..b41d3666 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,11 @@ dist/
node_modules/
thumbs.db
.idea/
-app/
\ No newline at end of file
+app/
+build/bin
+snippets/
+.task/
+*.md
+!README.md
+!CONTRIBUTING.md
+*.log
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 5071bdc4..00000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "eslint.validate": [ "javascript", "svelte" ]
-}
\ No newline at end of file
diff --git a/.yarnclean b/.yarnclean
deleted file mode 100644
index 09cfc9ec..00000000
--- a/.yarnclean
+++ /dev/null
@@ -1,39 +0,0 @@
-# test directories
-__tests__
-node_modules/*/test
-node_modules/*/tests
-powered-test
-
-# asset directories
-docs
-doc
-website
-images
-
-# examples
-example
-examples
-
-# code coverage directories
-coverage
-.nyc_output
-
-# build scripts
-Makefile
-Gulpfile.js
-Gruntfile.js
-
-# configs
-.tern-project
-.gitattributes
-.editorconfig
-.*ignore
-.eslintrc
-.jshintrc
-.flowconfig
-.documentup.json
-.yarn-metadata.json
-
-# misc
-*.gz
-*.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1b08f7fe..6646794d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,6 +9,7 @@ The following is a set of guidelines for contributing to BetterDiscord's Install
[Code of Conduct](#code-of-conduct)
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
+ * [Development Setup](#development-setup)
[How Can I Contribute?](#how-can-i-contribute)
* [Reporting Bugs](#reporting-bugs)
@@ -18,7 +19,8 @@ The following is a set of guidelines for contributing to BetterDiscord's Install
[Styleguides](#styleguides)
* [Git Commit Messages](#git-commit-messages)
- * [JavaScript Styleguide](#javascript-styleguide)
+ * [Frontend (Svelte + TypeScript) Styleguide](#frontend-svelte--typescript-styleguide)
+ * [Go Styleguide](#go-styleguide)
[Additional Notes](#additional-notes)
* [Issue Labels](#issue-labels)
@@ -29,33 +31,79 @@ This project and everyone participating in it is governed by the [Code of Conduc
## What should I know before I get started?
-
+This installer is built with [Wails](https://wails.io/) (Go backend + Svelte frontend). The Go runtime embeds the compiled frontend assets from `frontend/build` (see `//go:embed` in `main.go`).
+
+The repository is organized into:
+
+```
+.
+├── api // Wails bindings and backend runtime helpers.
+├── build // Build assets and platform-specific packaging files.
+├── frontend // Svelte UI bundled by Vite (Bun-powered).
+├── types // Shared Go types used by bindings.
+├── utils // Backend utilities.
+├── main.go // Wails application entry point.
+├── wails.json // Wails configuration + frontend hooks.
+```
+
+### Development Setup
+
+Prerequisites:
+
+* [Go](https://go.dev/) (matches `go.mod`)
+* [Bun](https://bun.sh/)
+* [Wails CLI](https://wails.io/docs/gettingstarted/installation)
+
+Linux prerequisites:
+
+* Wails requires GTK/WebKit and build tooling. Follow the official Linux dependencies guide:
+ https://wails.io/docs/gettingstarted/installation#linux
+* Fedora note: you may need to set `PKG_CONFIG_PATH` to include system pkgconfig directories:
+ ```sh
+ export PKG_CONFIG_PATH="/usr/lib64/pkgconfig:/usr/share/pkgconfig"
+ ```
+
+Common commands:
+
+* `wails dev` - run the app locally with backend bindings.
+* `wails build` - build distributable binaries.
+* `bun run --bun eslint .` (from `frontend/`) - lint frontend changes.
+* `bun run --bun svelte-check --tsconfig ./tsconfig.json` (from `frontend/`) - static checks.
+* `go test ./...` - run backend tests.
+* `gofmt -w .` - format Go files you touch.
+
+Build and development details:
+
+* Frontend assets are built by Vite into `frontend/build` and embedded by Go via `//go:embed` in `main.go`.
+* Wails uses the hooks in `wails.json` to install and build frontend assets during `wails dev` and `wails build`.
## How Can I Contribute?
### Reporting Bugs
-
+Please search for existing issues first. If you find a match, add any extra context you have (logs, OS details, screenshots).
#### Before Submitting A Bug Report
-
+* Reproduce on the latest `development` branch or a recent release.
+* Collect logs from the installer UI (copy any error output shown).
+* Note your OS and architecture (Windows/macOS/Linux, x64/arm64).
#### How Do I Submit A (Good) Bug Report?
-
+Include clear steps to reproduce, expected vs. actual behavior, and any logs shown in the UI.
### Suggesting Enhancements
-
+Open an issue describing the problem you are trying to solve, the proposed solution, and any alternatives considered.
#### Before Submitting An Enhancement Suggestion
-
+Confirm the change aligns with the existing installer flow and doesn't require a new runtime permission without a clear UX plan.
#### How Do I Submit A (Good) Enhancement Suggestion?
-
+Provide context, screenshots/mockups (if UI changes), and a minimal spec of expected behavior.
### Your First Code Contribution
@@ -81,9 +129,9 @@ While the prerequisites above must be satisfied prior to having your pull reques
* Reference issues and pull requests liberally after the first line
* When only changing documentation, include `[ci skip]` in the commit title
-### JavaScript Styleguide
+### Frontend (Svelte + TypeScript) Styleguide
-All JavaScript must adhere to the [ESLint rules](https://github.com/BetterDiscord/Installer/blob/main/.eslintrc) of the repo.
+All frontend code must adhere to the [ESLint rules](https://github.com/BetterDiscord/Installer/blob/main/frontend/eslint.config.js) of the repo.
Some other style related points not covered by ESLint:
@@ -104,13 +152,11 @@ Some other style related points not covered by ESLint:
* Place class properties in the following order:
* Class methods and properties (methods starting with `static`)
* Instance methods and properties
-* Place requires in the following order:
- * Built in Node Modules (such as `path`)
- * Repo level global imports (such as `modules`, `builtins`)
- * Local Modules (using relative paths)
-* Prefer to import whole modules instead of singular functions
- * Keep modules namespaced and organized
- * This includes Node Modules (such as `fs`)
+* Place imports in the following order:
+ * Built in modules (such as `path`)
+ * Third-party dependencies
+ * Local modules (relative paths or `$lib` aliases)
+* Prefer to import whole modules instead of singular functions when working in Node/Go tooling
```js
const fs = require("fs"); // Use this
const {readFile, writeFile} = require("fs"); // Avoid this
@@ -119,12 +165,31 @@ import Utilities from "./utilities"; // Use this
import {deepclone, isEmpty} from "./utilties"; // Avoid this
```
+### Go Styleguide
+
+* Run `gofmt` on any Go files you touch.
+* Keep standard library imports first, then a blank line, then external deps, then local packages.
+* Prefer early returns for error handling and emit Wails events (`log`, `success`, `failure`, `reset`) instead of silently swallowing failures.
+
## Additional Notes
+### Releases
+
+Releases are tag-driven. Create a tag like `vX.Y.Z` and push it to trigger the release workflow. The workflow uses the tag to:
+
+* Set the runtime version via `-ldflags="-X main.version=vX.Y.Z"`.
+* Override `wails.json` `info.productVersion` during CI builds so Windows metadata matches the tag.
+
+If you build locally for a release, pass the same ldflags and update `wails.json` to keep metadata aligned:
+
+```sh
+wails build -ldflags="-X main.version=vX.Y.Z"
+```
+
### Issue Labels
-
+Use labels to clarify impact (`bug`, `enhancement`, `maintenance`) and scope (`frontend`, `backend`, `release`).
#### Type of Issue and Issue State
-
+If you can, include an expected fix timeline or milestone to help triage.
diff --git a/README.md b/README.md
index e3c0e8ac..ad07461b 100644
--- a/README.md
+++ b/README.md
@@ -27,13 +27,13 @@
# Overview
-This repository contains the source code for the BetterDiscord installer. This installer is written with [electron-webpack](https://webpack.electron.build/) and [Svelte 3](https://svelte.dev/).
+This repository contains the source code for the BetterDiscord installer. The app is built with [Wails](https://wails.io/) (Go backend + Svelte frontend) and ships as a native desktop application.
## Downloads
These will link you to the latest builds found in the [releases](https://github.com/BetterDiscord/installer/releases/) tab of this repository.
-| [Windows (7+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Windows.exe) | [macOS (10.10+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Mac.zip) | [Linux](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Linux.AppImage) |
+| [Windows (7+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Windows.exe) | [macOS (10.10+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Mac.zip) | [Linux](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Linux) |
| ------------- | ------------- | ------------- |
@@ -42,19 +42,13 @@ These will link you to the latest builds found in the [releases](https://github.
```
.
-├──assets // Contains static assets (such as images) used by the installer.
-| └──images // Images (logos, backgrounds, etc...) used by the installer.
-├──scripts // Scripts needed for development and contributing.
-└──src // The installer's source code.
- ├──main // Electron "main" process. Creates and configures the BrowserWindow.
- └──renderer // Electron "renderer" process. Contains most components and scripts.
- ├──actions // Scripts performed by the installer such as installing, repairing and uninstalling.
- | └──utils // Common utilities used by installer actions (such as killing discord).
- ├──common // Common UI components such as buttons, checkboxes, radios, etc...
- ├──pages // Component files for each page in the installer's setup process.
- ├──stores // Svelte store used for storing global data.
- | └──types // Used for defining custom svelte stores.
- └──transitions // Contains custom Svelte transitions and animations.
+├── api // Wails bindings and backend runtime helpers.
+├── build // Build assets and platform-specific packaging files.
+├── frontend // Svelte UI bundled by Vite (Bun-powered).
+├── types // Shared Go types used by bindings.
+├── utils // Backend utilities.
+├── main.go // Wails application entry point.
+├── wails.json // Wails configuration + frontend hooks.
```
---
@@ -65,10 +59,23 @@ These will link you to the latest builds found in the [releases](https://github.
## Prerequisites
- [Git](https://git-scm.com)
-- [Node.js](https://nodejs.org/en/)
-- [yarn](https://yarnpkg.com)
+- [Go](https://go.dev/) (matches `go.mod`)
+- [Bun](https://bun.sh/)
+- [Wails CLI](https://wails.io/docs/gettingstarted/installation)
- Command line of your choice.
+### Linux prerequisites
+
+Wails requires additional system packages on Linux (GTK/WebKit and build tooling). See the official Wails Linux dependencies guide:
+
+https://wails.io/docs/gettingstarted/installation#linux
+
+Fedora note: you may need to set `PKG_CONFIG_PATH` to include system pkgconfig directories:
+
+```sh
+export PKG_CONFIG_PATH="/usr/lib64/pkgconfig:/usr/share/pkgconfig"
+```
+
## Building
### 1: Clone the repository.
@@ -77,32 +84,48 @@ git clone https://github.com/BetterDiscord/installer && cd installer
```
This will create a local copy of this repository and navigate you to the root folder of the repository.
-### 2: Install Dependencies
-Run this command at the root folder to install dependencies:
+### Quick Start (Development)
+
+To run the installer locally with backend bindings:
+
```ps
-yarn install
+wails dev
```
-### 3: Run Build Script
-To run the installer in development mode, simply run the following command:
+To build a distributable binary:
+
```ps
-yarn dev
+wails build
```
## Additional Scripts
-### Linting
-This project uses [ESLint](https://eslint.org/). Run this command to lint your changes:
+### Frontend checks
+
```ps
-yarn lint
+cd frontend
+bun install
+bun run --bun svelte-kit sync
+bun run --bun svelte-check --tsconfig ./tsconfig.json
+bun run --bun eslint .
```
-### Compiling & Distribution
+### Frontend tests
```ps
-yarn dist
+cd frontend
+bun install
+bun run test
```
+### Backend checks
+
+```ps
+go test ./...
+go fmt ./...
+```
+
+
---
# Contributors
diff --git a/api/controller.go b/api/controller.go
new file mode 100644
index 00000000..986ca841
--- /dev/null
+++ b/api/controller.go
@@ -0,0 +1,172 @@
+package api
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ "installer/discord"
+ "installer/types"
+
+ "github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+type Controller struct {
+ ctx context.Context
+}
+
+func NewController() *Controller {
+ return &Controller{}
+}
+
+// SetContext is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (action *Controller) SetContext(ctx context.Context) {
+ action.ctx = ctx
+}
+
+func (a *Controller) Write(p []byte) (n int, err error) {
+ fmt.Println(string(p[:]))
+ runtime.EventsEmit(a.ctx, "log", string(p[:]))
+ return len(p), nil
+}
+
+func (d *Controller) GetDiscordPath(channel string) string {
+ return discord.GetSuggestedPath(types.ParseChannel(channel))
+}
+
+// #region Actions
+func (action *Controller) Install(corePaths []string, options types.InstallOptions) {
+ for i := range corePaths {
+ install := discord.ResolvePath(corePaths[i])
+ if install == nil {
+ continue
+ }
+
+ if err := install.InstallBD(options); err != nil {
+ runtime.EventsEmit(action.ctx, "failure")
+ return
+ }
+ }
+
+ runtime.EventsEmit(action.ctx, "success")
+}
+
+func (action *Controller) Uninstall(corePaths []string, options types.UninstallOptions) {
+ for i := range corePaths {
+ install := discord.ResolvePath(corePaths[i])
+ if install == nil {
+ continue
+ }
+
+ if err := install.UninstallBD(options); err != nil {
+ runtime.EventsEmit(action.ctx, "failure")
+ return
+ }
+ }
+
+ runtime.EventsEmit(action.ctx, "success")
+}
+
+func (action *Controller) Repair(corePaths []string, options types.RepairOptions) {
+ for i := range corePaths {
+ install := discord.ResolvePath(corePaths[i])
+ if install == nil {
+ continue
+ }
+
+ if err := install.RepairBD(options); err != nil {
+ runtime.EventsEmit(action.ctx, "failure")
+ return
+ }
+ }
+
+ result, err := runtime.MessageDialog(action.ctx, runtime.MessageDialogOptions{
+ Type: runtime.QuestionDialog,
+ Title: "Repair Complete",
+ Message: "Repair is complete. Would you like to reinstall BetterDiscord now?",
+ DefaultButton: "Yes",
+ })
+
+ if err != nil {
+ runtime.EventsEmit(action.ctx, "success")
+ return
+ }
+
+ if result == "Yes" {
+ runtime.EventsEmit(action.ctx, "reset")
+ runtime.EventsEmit(action.ctx, "navigate", map[string]string{
+ "action": "install",
+ })
+ return
+ }
+
+ runtime.EventsEmit(action.ctx, "success")
+}
+
+// #endregion
+
+// #region Dialogs
+func (d *Controller) BrowseForDiscord(schannel string) string {
+ var browsePath string
+ browsePath, err := os.UserConfigDir()
+ if err != nil {
+ browsePath = os.Getenv("HOME")
+ }
+
+ channel := types.ParseChannel(schannel)
+
+ selection, err := runtime.OpenDirectoryDialog(d.ctx, runtime.OpenDialogOptions{
+ Title: "Browsing to " + channel.Name(),
+ DefaultDirectory: browsePath,
+ ShowHiddenFiles: true,
+ TreatPackagesAsDirectories: true,
+ })
+
+ if err != nil || selection == "" {
+ return ""
+ }
+
+ if result := discord.ResolvePath(selection); result != nil {
+ return result.CorePath
+ }
+
+ return ""
+}
+
+func (d *Controller) ConfirmAction(title string, message string) string {
+ result, err := runtime.MessageDialog(d.ctx, runtime.MessageDialogOptions{
+ Type: runtime.QuestionDialog,
+ Title: title,
+ Message: message,
+ DefaultButton: "No",
+ })
+
+ if err != nil {
+ return ""
+ }
+
+ return result
+}
+
+func (d *Controller) ShowNotice(dialog string, title string, message string) string {
+
+ dialogType := runtime.InfoDialog
+ if dialog == "error" {
+ dialogType = runtime.ErrorDialog
+ }
+
+ result, err := runtime.MessageDialog(d.ctx, runtime.MessageDialogOptions{
+ Type: dialogType,
+ Title: title,
+ Message: message,
+ })
+
+ if err != nil {
+ return ""
+ }
+
+ return result
+}
+
+// #endregion
diff --git a/app.go b/app.go
new file mode 100644
index 00000000..65684915
--- /dev/null
+++ b/app.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "installer/types"
+ "installer/utils"
+
+ "github.com/pkg/browser"
+ "github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+type App struct {
+ ctx context.Context
+}
+
+func NewApp() *App {
+ return &App{}
+}
+
+func (a *App) SetContext(ctx context.Context) {
+ a.ctx = ctx
+}
+
+func (a *App) GetVersion() string {
+ return version
+}
+
+func (a *App) CheckForUpdate() {
+ // If the version doesn't start with "v" then it's a
+ // development build, so we don't check for updates
+ if !strings.HasPrefix(version, "v") {
+ return
+ }
+
+ // Get latest installer version from GitHub API
+ apiData, err := utils.DownloadJSON[types.GitHubRelease]("https://api.github.com/repos/BetterDiscord/Installer/releases/latest")
+ if err != nil {
+ return
+ }
+
+ // If the current version is greater than or equal
+ // to the latest version, no update is needed
+ if utils.CompareVersions(version, apiData.TagName) >= 0 {
+ return
+ }
+
+ result, err := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
+ Type: runtime.QuestionDialog,
+ Title: "Update Available",
+ Message: fmt.Sprintf("A new version (%s) of the installer is available. Would you like to download it now?", apiData.TagName),
+ DefaultButton: "Yes",
+ })
+
+ if err != nil {
+ return
+ }
+
+ if result == "Yes" {
+ browser.OpenURL(apiData.HTMLURL)
+ }
+}
diff --git a/assets/images/splash.bmp b/assets/images/splash.bmp
deleted file mode 100644
index ddef694e..00000000
Binary files a/assets/images/splash.bmp and /dev/null differ
diff --git a/betterdiscord/download.go b/betterdiscord/download.go
new file mode 100644
index 00000000..4c4ece7f
--- /dev/null
+++ b/betterdiscord/download.go
@@ -0,0 +1,77 @@
+package betterdiscord
+
+import (
+ "fmt"
+ "installer/types"
+ "installer/utils"
+ "log"
+)
+
+func (i *BDInstall) download() error {
+ if i.hasDownloaded {
+ log.Printf("✅ Already downloaded to %s\n", i.asar)
+ return nil
+ }
+
+ resp, err := utils.DownloadFile("https://betterdiscord.app/Download/betterdiscord.asar", i.asar)
+ if err == nil {
+ version := resp.Header.Get("x-bd-version")
+ if version == "" {
+ log.Println("✅ Downloaded BetterDiscord from the official website")
+ } else {
+ log.Printf("✅ Downloaded BetterDiscord version %s from the official website\n", utils.FormatVersion(version))
+ }
+ i.hasDownloaded = true
+ return nil
+ } else {
+ log.Println("❌ Failed to download BetterDiscord from official website")
+ log.Printf("❌ %s\n", err.Error())
+ log.Println("")
+ log.Println("🔁 Falling back to GitHub...")
+ }
+
+ // Get download URL from GitHub API
+ apiData, err := utils.DownloadJSON[types.GitHubRelease]("https://api.github.com/repos/BetterDiscord/BetterDiscord/releases/latest")
+ if err != nil {
+ log.Println("❌ Failed to get asset url from GitHub")
+ log.Printf("❌ %s\n", err.Error())
+ return err
+ }
+
+ var index = -1
+ for idx, asset := range apiData.Assets {
+ if asset.Name == "betterdiscord.asar" {
+ index = idx
+ break
+ }
+ }
+
+ if index == -1 {
+ log.Println("❌ Failed to find the BetterDiscord asar on GitHub")
+ return fmt.Errorf("failed to find betterdiscord.asar asset in GitHub release")
+ }
+
+ var downloadUrl = apiData.Assets[index].URL
+ var version = apiData.TagName
+
+ if downloadUrl != "" {
+ log.Printf("✅ Found BetterDiscord: %s\n", downloadUrl)
+ }
+
+ // Download asar into the BD folder
+ _, err = utils.DownloadFile(downloadUrl, i.asar)
+ if err != nil {
+ log.Println("❌ Failed to download BetterDiscord from GitHub")
+ log.Printf("❌ %s\n", err.Error())
+ return err
+ }
+
+ if version == "" {
+ log.Println("✅ Downloaded BetterDiscord from GitHub")
+ } else {
+ log.Printf("✅ Downloaded BetterDiscord version %s from GitHub\n", utils.FormatVersion(version))
+ }
+ i.hasDownloaded = true
+
+ return nil
+}
diff --git a/betterdiscord/install.go b/betterdiscord/install.go
new file mode 100644
index 00000000..d862d5d5
--- /dev/null
+++ b/betterdiscord/install.go
@@ -0,0 +1,131 @@
+package betterdiscord
+
+import (
+ "installer/types"
+ "installer/utils"
+ "installer/wsl"
+ "log"
+ "os"
+ "path/filepath"
+ "sync"
+)
+
+type BDInstall struct {
+ root string
+ data string
+ asar string
+ plugins string
+ themes string
+ hasDownloaded bool
+}
+
+// Root returns the root directory path of the BetterDiscord installation
+func (i *BDInstall) Root() string {
+ return i.root
+}
+
+// Data returns the data directory path
+func (i *BDInstall) Data() string {
+ return i.data
+}
+
+// Asar returns the path to the BetterDiscord asar file
+func (i *BDInstall) Asar() string {
+ return i.asar
+}
+
+// Plugins returns the plugins directory path
+func (i *BDInstall) Plugins() string {
+ return i.plugins
+}
+
+// Themes returns the themes directory path
+func (i *BDInstall) Themes() string {
+ return i.themes
+}
+
+// HasDownloaded returns whether BetterDiscord has been downloaded
+func (i *BDInstall) HasDownloaded() bool {
+ return i.hasDownloaded
+}
+
+// Download downloads the BetterDiscord asar file
+func (i *BDInstall) Download() error {
+ return i.download()
+}
+
+// Prepare creates all necessary directories for BetterDiscord
+func (i *BDInstall) Prepare() error {
+ return i.prepare()
+}
+
+// Repair repairs the BetterDiscord installation with specific options
+func (i *BDInstall) Repair(channel types.DiscordChannel, options types.RepairOptions) error {
+ return i.repair(channel, options)
+}
+
+func (i *BDInstall) IsAsarInstalled() bool {
+ return utils.Exists(i.asar)
+}
+
+// RemoveAll deletes the BetterDiscord installation directory and all contents.
+func (i *BDInstall) RemoveAll() error {
+ if !utils.Exists(i.root) {
+ log.Printf("✅ BetterDiscord folder not found: %s\n", i.root)
+ return nil
+ }
+
+ if err := os.RemoveAll(i.root); err != nil {
+ log.Printf("❌ Failed to remove BetterDiscord folder: %s\n", i.root)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ log.Printf("✅ Removed BetterDiscord folder: %s\n", i.root)
+ return nil
+}
+
+var lock = &sync.Mutex{}
+var globalInstance *BDInstall
+
+func GetInstallation(base ...string) *BDInstall {
+ if len(base) == 0 {
+ if globalInstance != nil {
+ return globalInstance
+ }
+
+ lock.Lock()
+ defer lock.Unlock()
+ if globalInstance != nil {
+ return globalInstance
+ }
+
+ // Default to user config directory
+ configDir, _ := os.UserConfigDir()
+
+ // Handle WSL with Windows home directory
+ if wsl.IsWSL() {
+ winHome, err := wsl.WindowsHome()
+ if err == nil && winHome != "" {
+ configDir = filepath.Join(winHome, "AppData", "Roaming")
+ }
+ }
+
+ globalInstance = GetInstallation(configDir)
+
+ return globalInstance
+ }
+
+ return New(filepath.Join(base[0], "BetterDiscord"))
+}
+
+func New(root string) *BDInstall {
+ return &BDInstall{
+ root: root,
+ data: filepath.Join(root, "data"),
+ asar: filepath.Join(root, "data", "betterdiscord.asar"),
+ plugins: filepath.Join(root, "plugins"),
+ themes: filepath.Join(root, "themes"),
+ hasDownloaded: false,
+ }
+}
diff --git a/betterdiscord/install_test.go b/betterdiscord/install_test.go
new file mode 100644
index 00000000..ae2a5371
--- /dev/null
+++ b/betterdiscord/install_test.go
@@ -0,0 +1,431 @@
+package betterdiscord
+
+import (
+ "installer/types"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestNew(t *testing.T) {
+ rootPath := "/test/root/BetterDiscord"
+ install := New(rootPath)
+
+ if install.Root() != rootPath {
+ t.Errorf("Root() = %s, expected %s", install.Root(), rootPath)
+ }
+
+ expectedData := filepath.Join(rootPath, "data")
+ if install.Data() != expectedData {
+ t.Errorf("Data() = %s, expected %s", install.Data(), expectedData)
+ }
+
+ expectedAsar := filepath.Join(rootPath, "data", "betterdiscord.asar")
+ if install.Asar() != expectedAsar {
+ t.Errorf("Asar() = %s, expected %s", install.Asar(), expectedAsar)
+ }
+
+ expectedPlugins := filepath.Join(rootPath, "plugins")
+ if install.Plugins() != expectedPlugins {
+ t.Errorf("Plugins() = %s, expected %s", install.Plugins(), expectedPlugins)
+ }
+
+ expectedThemes := filepath.Join(rootPath, "themes")
+ if install.Themes() != expectedThemes {
+ t.Errorf("Themes() = %s, expected %s", install.Themes(), expectedThemes)
+ }
+
+ if install.HasDownloaded() {
+ t.Error("HasDownloaded() should be false for new install")
+ }
+}
+
+func TestBDInstall_GettersSetters(t *testing.T) {
+ rootPath := "/test/path"
+ install := New(rootPath)
+
+ // Test all getters return expected paths
+ tests := []struct {
+ name string
+ getter func() string
+ expected string
+ }{
+ {"Root", install.Root, rootPath},
+ {"Data", install.Data, filepath.Join(rootPath, "data")},
+ {"Asar", install.Asar, filepath.Join(rootPath, "data", "betterdiscord.asar")},
+ {"Plugins", install.Plugins, filepath.Join(rootPath, "plugins")},
+ {"Themes", install.Themes, filepath.Join(rootPath, "themes")},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := tt.getter()
+ if result != tt.expected {
+ t.Errorf("%s() = %s, expected %s", tt.name, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestBDInstall_Prepare(t *testing.T) {
+ // Create temporary directory for testing
+ tmpDir := t.TempDir()
+ bdRoot := filepath.Join(tmpDir, "BetterDiscord")
+
+ install := New(bdRoot)
+
+ // Prepare should create all necessary directories
+ err := install.Prepare()
+ if err != nil {
+ t.Fatalf("Prepare() failed: %v", err)
+ }
+
+ // Verify directories were created
+ dirs := []string{
+ install.Data(),
+ install.Plugins(),
+ install.Themes(),
+ }
+
+ for _, dir := range dirs {
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
+ t.Errorf("Directory not created: %s", dir)
+ }
+ }
+}
+
+func TestBDInstall_Prepare_AlreadyExists(t *testing.T) {
+ // Create temporary directory with existing structure
+ tmpDir := t.TempDir()
+ bdRoot := filepath.Join(tmpDir, "BetterDiscord")
+
+ install := New(bdRoot)
+
+ // Create directories manually first
+ os.MkdirAll(install.Data(), 0755) //nolint:errcheck
+ os.MkdirAll(install.Plugins(), 0755) //nolint:errcheck
+ os.MkdirAll(install.Themes(), 0755) //nolint:errcheck
+
+ // Prepare should succeed even if directories already exist
+ err := install.Prepare()
+ if err != nil {
+ t.Fatalf("Prepare() failed when directories already exist: %v", err)
+ }
+
+ // Verify directories still exist
+ dirs := []string{
+ install.Data(),
+ install.Plugins(),
+ install.Themes(),
+ }
+
+ for _, dir := range dirs {
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
+ t.Errorf("Directory should exist: %s", dir)
+ }
+ }
+}
+
+func TestBDInstall_Repair(t *testing.T) {
+ // Create temporary directory for testing
+ tmpDir := t.TempDir()
+ bdRoot := filepath.Join(tmpDir, "BetterDiscord")
+
+ install := New(bdRoot)
+
+ // Create the data directory and a test plugins.json file
+ channelFolder := filepath.Join(install.Data(), types.Stable.String())
+ os.MkdirAll(channelFolder, 0755) //nolint:errcheck
+
+ pluginsJson := filepath.Join(channelFolder, "plugins.json")
+ err := os.WriteFile(pluginsJson, []byte(`{"test": "data"}`), 0644)
+ if err != nil {
+ t.Fatalf("Failed to create test plugins.json: %v", err)
+ }
+
+ // Verify file exists before repair
+ if _, err := os.Stat(pluginsJson); os.IsNotExist(err) {
+ t.Fatal("plugins.json should exist before repair")
+ }
+
+ // Run repair
+ err = install.Repair(types.Stable, types.RepairOptions{DisablePlugins: true})
+ if err != nil {
+ t.Fatalf("Repair() failed: %v", err)
+ }
+
+ // Verify file was removed
+ if _, err := os.Stat(pluginsJson); !os.IsNotExist(err) {
+ t.Error("plugins.json should be removed after repair")
+ }
+}
+
+func TestBDInstall_Repair_NoPluginsFile(t *testing.T) {
+ // Create temporary directory for testing
+ tmpDir := t.TempDir()
+ bdRoot := filepath.Join(tmpDir, "BetterDiscord")
+
+ install := New(bdRoot)
+
+ // Don't create any files - repair should succeed without error
+ err := install.Repair(types.Stable, types.RepairOptions{DisablePlugins: true})
+ if err != nil {
+ t.Fatalf("Repair() should succeed when plugins.json doesn't exist: %v", err)
+ }
+}
+
+func TestBDInstall_Repair_MultipleChannels(t *testing.T) {
+ // Create temporary directory for testing
+ tmpDir := t.TempDir()
+ bdRoot := filepath.Join(tmpDir, "BetterDiscord")
+
+ install := New(bdRoot)
+
+ // Create plugins.json for multiple channels
+ channels := []types.DiscordChannel{types.Stable, types.Canary, types.PTB}
+ pluginsFiles := make(map[types.DiscordChannel]string)
+
+ for _, channel := range channels {
+ channelFolder := filepath.Join(install.Data(), channel.String())
+ os.MkdirAll(channelFolder, 0755) //nolint:errcheck
+
+ pluginsJson := filepath.Join(channelFolder, "plugins.json")
+ os.WriteFile(pluginsJson, []byte(`{}`), 0644) //nolint:errcheck
+ pluginsFiles[channel] = pluginsJson
+ }
+
+ // Repair only Stable channel
+ err := install.Repair(types.Stable, types.RepairOptions{DisablePlugins: true})
+ if err != nil {
+ t.Fatalf("Repair(Stable) failed: %v", err)
+ }
+
+ // Verify only Stable's plugins.json was removed
+ if _, err := os.Stat(pluginsFiles[types.Stable]); !os.IsNotExist(err) {
+ t.Error("Stable plugins.json should be removed")
+ }
+
+ // Verify other channels' files still exist
+ if _, err := os.Stat(pluginsFiles[types.Canary]); os.IsNotExist(err) {
+ t.Error("Canary plugins.json should still exist")
+ }
+ if _, err := os.Stat(pluginsFiles[types.PTB]); os.IsNotExist(err) {
+ t.Error("PTB plugins.json should still exist")
+ }
+}
+
+func TestBDInstall_RepairWithOptions_AllToggles(t *testing.T) {
+ tmpDir := t.TempDir()
+ bdRoot := filepath.Join(tmpDir, "BetterDiscord")
+
+ install := New(bdRoot)
+ channel := types.Stable
+ channelFolder := filepath.Join(install.Data(), channel.String())
+ if err := os.MkdirAll(channelFolder, 0755); err != nil {
+ t.Fatalf("Failed to create channel folder: %v", err)
+ }
+
+ paths := []string{
+ filepath.Join(channelFolder, "plugins.json"),
+ filepath.Join(channelFolder, "themes.json"),
+ filepath.Join(channelFolder, "custom.css"),
+ filepath.Join(channelFolder, "settings.json"),
+ filepath.Join(channelFolder, "webpack.json"),
+ filepath.Join(channelFolder, "addon-store.json"),
+ }
+ for _, path := range paths {
+ if err := os.WriteFile(path, []byte("{}"), 0644); err != nil {
+ t.Fatalf("Failed to create file %s: %v", path, err)
+ }
+ }
+
+ options := types.RepairOptions{
+ DisablePlugins: true,
+ DisableThemes: true,
+ ClearCustomCSS: true,
+ ClearWebpackCache: true,
+ ClearAddonStoreCache: true,
+ ResetSettings: true,
+ }
+
+ if err := install.Repair(channel, options); err != nil {
+ t.Fatalf("RepairWithOptions() failed: %v", err)
+ }
+
+ for _, path := range paths {
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Errorf("Expected %s to be removed", path)
+ }
+ }
+
+ backupRoot := filepath.Join(channelFolder, "backup")
+ entries, err := os.ReadDir(backupRoot)
+ if err != nil {
+ t.Fatalf("Expected backup folder to exist: %v", err)
+ }
+ if len(entries) == 0 {
+ t.Fatalf("Expected at least one backup folder")
+ }
+ backupDir := filepath.Join(backupRoot, entries[0].Name())
+ if _, err := os.Stat(filepath.Join(backupDir, "custom.css")); err != nil {
+ t.Fatalf("Expected custom.css to be backed up: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(backupDir, "settings.json")); err != nil {
+ t.Fatalf("Expected settings.json to be backed up: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(backupDir, "plugins.json")); err != nil {
+ t.Fatalf("Expected plugins.json to be backed up: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(backupDir, "themes.json")); err != nil {
+ t.Fatalf("Expected themes.json to be backed up: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(backupDir, "webpack.json")); err != nil {
+ t.Fatalf("Expected webpack.json to be backed up: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(backupDir, "addon-store.json")); err != nil {
+ t.Fatalf("Expected addon-store.json to be backed up: %v", err)
+ }
+}
+
+func TestBDInstall_RepairWithOptions_PartialToggles(t *testing.T) {
+ tmpDir := t.TempDir()
+ bdRoot := filepath.Join(tmpDir, "BetterDiscord")
+
+ install := New(bdRoot)
+ channel := types.Stable
+ channelFolder := filepath.Join(install.Data(), channel.String())
+ if err := os.MkdirAll(channelFolder, 0755); err != nil {
+ t.Fatalf("Failed to create channel folder: %v", err)
+ }
+
+ pluginsJson := filepath.Join(channelFolder, "plugins.json")
+ themesJson := filepath.Join(channelFolder, "themes.json")
+ if err := os.WriteFile(pluginsJson, []byte("{}"), 0644); err != nil {
+ t.Fatalf("Failed to create plugins.json: %v", err)
+ }
+ if err := os.WriteFile(themesJson, []byte("{}"), 0644); err != nil {
+ t.Fatalf("Failed to create themes.json: %v", err)
+ }
+
+ options := types.RepairOptions{
+ DisablePlugins: true,
+ DisableThemes: false,
+ }
+
+ if err := install.Repair(channel, options); err != nil {
+ t.Fatalf("RepairWithOptions() failed: %v", err)
+ }
+
+ if _, err := os.Stat(pluginsJson); !os.IsNotExist(err) {
+ t.Errorf("Expected plugins.json to be removed")
+ }
+ if _, err := os.Stat(themesJson); os.IsNotExist(err) {
+ t.Errorf("Expected themes.json to remain")
+ }
+}
+
+func TestGetInstallation_WithBase(t *testing.T) {
+ basePath := "/test/config"
+ install := GetInstallation(basePath)
+
+ expectedRoot := filepath.Join(basePath, "BetterDiscord")
+ if install.Root() != expectedRoot {
+ t.Errorf("GetInstallation(%s).Root() = %s, expected %s", basePath, install.Root(), expectedRoot)
+ }
+}
+
+func TestGetInstallation_Singleton(t *testing.T) {
+ // Reset the global instance to ensure clean test
+ // Note: In a real test, you might want to add a reset function
+ // For now, we'll just test that multiple calls work
+
+ install1 := GetInstallation()
+ install2 := GetInstallation()
+
+ // Both should return the same instance (singleton pattern)
+ if install1 != install2 {
+ t.Error("GetInstallation() should return the same instance (singleton)")
+ }
+
+ // Both should have the same root path
+ if install1.Root() != install2.Root() {
+ t.Errorf("Singleton instances have different roots: %s vs %s", install1.Root(), install2.Root())
+ }
+}
+
+func TestMakeDirectory(t *testing.T) {
+ tmpDir := t.TempDir()
+ testDir := filepath.Join(tmpDir, "test", "nested", "directory")
+
+ // Make the directory
+ err := makeDirectory(testDir)
+ if err != nil {
+ t.Fatalf("makeDirectory() failed: %v", err)
+ }
+
+ // Verify it exists
+ if _, err := os.Stat(testDir); os.IsNotExist(err) {
+ t.Error("Directory was not created")
+ }
+
+ // Test making the same directory again (should not error)
+ err = makeDirectory(testDir)
+ if err != nil {
+ t.Errorf("makeDirectory() should succeed when directory already exists: %v", err)
+ }
+}
+
+func TestBDInstall_HasDownloaded(t *testing.T) {
+ install := New("/test/path")
+
+ // Initially should be false
+ if install.HasDownloaded() {
+ t.Error("HasDownloaded() should initially be false")
+ }
+
+ // After setting hasDownloaded (internal state)
+ install.hasDownloaded = true
+ if !install.HasDownloaded() {
+ t.Error("HasDownloaded() should be true after download")
+ }
+}
+
+func TestBDInstall_PathStructure(t *testing.T) {
+ // Test with different root paths
+ tests := []struct {
+ name string
+ rootPath string
+ }{
+ {"Unix absolute path", "/home/user/.config/BetterDiscord"},
+ {"Windows absolute path", "C:\\Users\\User\\AppData\\Roaming\\BetterDiscord"},
+ {"Relative path", "BetterDiscord"},
+ {"Path with spaces", "/path with spaces/BetterDiscord"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ install := New(tt.rootPath)
+
+ // Verify all paths are constructed correctly relative to root
+ if install.Root() != tt.rootPath {
+ t.Errorf("Root() incorrect")
+ }
+
+ if install.Data() != filepath.Join(tt.rootPath, "data") {
+ t.Errorf("Data() path incorrect: %s", install.Data())
+ }
+
+ if install.Asar() != filepath.Join(tt.rootPath, "data", "betterdiscord.asar") {
+ t.Errorf("Asar() path incorrect: %s", install.Asar())
+ }
+
+ if install.Plugins() != filepath.Join(tt.rootPath, "plugins") {
+ t.Errorf("Plugins() path incorrect: %s", install.Plugins())
+ }
+
+ if install.Themes() != filepath.Join(tt.rootPath, "themes") {
+ t.Errorf("Themes() path incorrect: %s", install.Themes())
+ }
+ })
+ }
+}
diff --git a/betterdiscord/setup.go b/betterdiscord/setup.go
new file mode 100644
index 00000000..d3764b18
--- /dev/null
+++ b/betterdiscord/setup.go
@@ -0,0 +1,169 @@
+package betterdiscord
+
+import (
+ "installer/types"
+ "installer/utils"
+ "log"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+func makeDirectory(folder string) error {
+ exists := utils.Exists(folder)
+
+ if exists {
+ log.Printf("✅ Directory exists: %s\n", folder)
+ return nil
+ }
+
+ if err := os.MkdirAll(folder, 0755); err != nil {
+ log.Printf("❌ Failed to create directory: %s\n", folder)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ log.Printf("✅ Directory created: %s\n", folder)
+ return nil
+}
+
+func (i *BDInstall) prepare() error {
+ if err := makeDirectory(i.data); err != nil {
+ return err
+ }
+ if err := makeDirectory(i.plugins); err != nil {
+ return err
+ }
+ if err := makeDirectory(i.themes); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (i *BDInstall) repair(channel types.DiscordChannel, options types.RepairOptions) error {
+ channelFolder := filepath.Join(i.data, channel.String())
+ backupRoot := filepath.Join(channelFolder, "backup")
+ backupStamp := time.Now().Format("20060102-150405")
+ backupDir := filepath.Join(backupRoot, "repair-"+backupStamp)
+
+ if options.DisablePlugins {
+ if err := backupAndRemoveFile(channelFolder, "plugins.json", "plugins", channel.Name(), backupDir, true); err != nil {
+ return err
+ }
+ }
+
+ if options.DisableThemes {
+ if err := backupAndRemoveFile(channelFolder, "themes.json", "themes", channel.Name(), backupDir, true); err != nil {
+ return err
+ }
+ }
+
+ if options.ClearCustomCSS {
+ if err := backupAndRemoveFile(channelFolder, "custom.css", "custom CSS", channel.Name(), backupDir, false); err != nil {
+ return err
+ }
+ }
+
+ if options.ClearWebpackCache {
+ if err := backupAndRemoveFile(channelFolder, "webpack.json", "webpack cache", channel.Name(), backupDir, true); err != nil {
+ return err
+ }
+ }
+
+ if options.ClearAddonStoreCache {
+ if err := backupAndRemoveFile(channelFolder, "addon-store.json", "addon store cache", channel.Name(), backupDir, true); err != nil {
+ return err
+ }
+ }
+
+ if options.ResetSettings {
+ if err := backupAndRemoveFile(channelFolder, "settings.json", "settings", channel.Name(), backupDir, false); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func removeFile(basePath string, filename string, label string, channelName string) error {
+ path := filepath.Join(basePath, filename)
+ if !utils.Exists(path) {
+ log.Printf("✅ No %s found for %s\n", label, channelName)
+ return nil
+ }
+
+ if err := os.Remove(path); err != nil {
+ log.Printf("❌ Unable to remove %s: %s\n", label, path)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ log.Printf("✅ Removed %s for %s\n", label, channelName)
+ return nil
+}
+
+func backupAndRemoveFile(basePath string, filename string, label string, channelName string, backupDir string, allowDeleteOnBackupFailure bool) error {
+ path := filepath.Join(basePath, filename)
+ if !utils.Exists(path) {
+ log.Printf("✅ No %s found for %s\n", label, channelName)
+ return nil
+ }
+
+ backupErr := backupFile(path, backupDir, label, channelName)
+ if backupErr != nil && !allowDeleteOnBackupFailure {
+ return backupErr
+ }
+
+ if backupErr != nil {
+ log.Printf("⚠️ Proceeding without backup for %s\n", label)
+ }
+
+ if err := os.Remove(path); err != nil {
+ log.Printf("❌ Unable to remove %s: %s\n", label, path)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+ log.Printf("✅ Removed %s for %s\n", label, channelName)
+ return nil
+}
+
+func backupFile(path string, backupDir string, label string, channelName string) error {
+ if err := os.MkdirAll(backupDir, 0755); err != nil {
+ log.Printf("❌ Unable to create backup folder: %s\n", backupDir)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ log.Printf("❌ Unable to read %s: %s\n", label, path)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ backupPath := filepath.Join(backupDir, filepath.Base(path))
+ if err := os.WriteFile(backupPath, contents, 0644); err != nil {
+ log.Printf("❌ Unable to backup %s to %s\n", label, backupPath)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+ log.Printf("✅ Backed up %s for %s\n", label, channelName)
+ return nil
+}
+
+func removeDir(basePath string, dirname string, label string, channelName string) error {
+ path := filepath.Join(basePath, dirname)
+ if !utils.Exists(path) {
+ log.Printf("✅ No %s found for %s\n", label, channelName)
+ return nil
+ }
+
+ if err := os.RemoveAll(path); err != nil {
+ log.Printf("❌ Unable to remove %s: %s\n", label, path)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ log.Printf("✅ Removed %s for %s\n", label, channelName)
+ return nil
+}
diff --git a/build/README.md b/build/README.md
new file mode 100644
index 00000000..1ae2f677
--- /dev/null
+++ b/build/README.md
@@ -0,0 +1,35 @@
+# Build Directory
+
+The build directory is used to house all the build files and assets for your application.
+
+The structure is:
+
+* bin - Output directory
+* darwin - macOS specific files
+* windows - Windows specific files
+
+## Mac
+
+The `darwin` directory holds files specific to Mac builds.
+These may be customised and used as part of the build. To return these files to the default state, simply delete them
+and
+build with `wails build`.
+
+The directory contains the following files:
+
+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
+
+## Windows
+
+The `windows` directory contains the manifest and rc files used when building with `wails build`.
+These may be customised for your application. To return these files to the default state, simply delete them and
+build with `wails build`.
+
+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
+ will be created using the `appicon.png` file in the build directory.
+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
+ as well as the application itself (right click the exe -> properties -> details)
+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file
diff --git a/build/appicon.png b/build/appicon.png
new file mode 100644
index 00000000..f554e0ac
Binary files /dev/null and b/build/appicon.png differ
diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist
new file mode 100644
index 00000000..04727c23
--- /dev/null
+++ b/build/darwin/Info.dev.plist
@@ -0,0 +1,68 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.Name}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist
new file mode 100644
index 00000000..19cc9370
--- /dev/null
+++ b/build/darwin/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.Name}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+
+
diff --git a/assets/icon.icns b/build/darwin/icon.icns
similarity index 100%
rename from assets/icon.icns
rename to build/darwin/icon.icns
diff --git a/assets/icon.ico b/build/windows/icon.ico
similarity index 100%
rename from assets/icon.ico
rename to build/windows/icon.ico
diff --git a/build/windows/info.json b/build/windows/info.json
new file mode 100644
index 00000000..9727946b
--- /dev/null
+++ b/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "{{.Info.ProductVersion}}"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "{{.Info.ProductVersion}}",
+ "CompanyName": "{{.Info.CompanyName}}",
+ "FileDescription": "{{.Info.ProductName}}",
+ "LegalCopyright": "{{.Info.Copyright}}",
+ "ProductName": "{{.Info.ProductName}}",
+ "Comments": "{{.Info.Comments}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi
new file mode 100644
index 00000000..654ae2e4
--- /dev/null
+++ b/build/windows/installer/project.nsi
@@ -0,0 +1,114 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
+###
+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+ !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+ !insertmacro wails.setShellContext
+
+ !insertmacro wails.webview2runtime
+
+ SetOutPath $INSTDIR
+
+ !insertmacro wails.files
+
+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+ !insertmacro wails.associateFiles
+ !insertmacro wails.associateCustomProtocols
+
+ !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+ !insertmacro wails.setShellContext
+
+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+ RMDir /r $INSTDIR
+
+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+ !insertmacro wails.unassociateFiles
+ !insertmacro wails.unassociateCustomProtocols
+
+ !insertmacro wails.deleteUninstaller
+SectionEnd
diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh
new file mode 100644
index 00000000..f9c0f885
--- /dev/null
+++ b/build/windows/installer/wails_tools.nsh
@@ -0,0 +1,249 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+ !define INFO_PROJECTNAME "{{.Name}}"
+!endif
+!ifndef INFO_COMPANYNAME
+ !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
+!endif
+!ifndef INFO_PRODUCTNAME
+ !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
+!endif
+!ifndef INFO_PRODUCTVERSION
+ !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
+!endif
+!ifndef INFO_COPYRIGHT
+ !define INFO_COPYRIGHT "{{.Info.Copyright}}"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+ !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+ !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+ !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "amd64_arm64"
+ !else
+ !define ARCH "amd64"
+ !endif
+!else
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "arm64"
+ !else
+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+ !endif
+!endif
+
+!macro wails.checkArchitecture
+ !ifndef WAILS_WIN10_REQUIRED
+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+ !endif
+
+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+ !endif
+
+ ${If} ${AtLeastWin10}
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ IfSilent silentArch notSilentArch
+ silentArch:
+ SetErrorLevel 65
+ Abort
+ notSilentArch:
+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+ Quit
+ ${else}
+ IfSilent silentWin notSilentWin
+ silentWin:
+ SetErrorLevel 64
+ Abort
+ notSilentWin:
+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+ Quit
+ ${EndIf}
+
+ ok:
+!macroend
+
+!macro wails.files
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+ ${EndIf}
+ !endif
+!macroend
+
+!macro wails.writeUninstaller
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+ Delete "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+!macro wails.setShellContext
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
+ SetShellVarContext all
+ ${else}
+ SetShellVarContext current
+ ${EndIf}
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+ !endif
+
+ SetRegView 64
+ # If the admin key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+ ${EndIf}
+
+ SetDetailsPrint both
+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+ SetDetailsPrint listonly
+
+ InitPluginsDir
+ CreateDirectory "$pluginsdir\webview2bootstrapper"
+ SetOutPath "$pluginsdir\webview2bootstrapper"
+ File "tmp\MicrosoftEdgeWebview2Setup.exe"
+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+ SetDetailsPrint both
+ ok:
+!macroend
+
+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
+
+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
+!macroend
+
+!macro wails.associateFiles
+ ; Create file associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ File "..\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro wails.unassociateFiles
+ ; Delete app associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
+
+ Delete "$INSTDIR\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
+!macroend
+
+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+!macroend
+
+!macro wails.associateCustomProtocols
+ ; Create custom protocols associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ {{end}}
+!macroend
+
+!macro wails.unassociateCustomProtocols
+ ; Delete app custom protocol associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
+ {{end}}
+!macroend
diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest
new file mode 100644
index 00000000..17e1a238
--- /dev/null
+++ b/build/windows/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/discord/assets/injection.js b/discord/assets/injection.js
new file mode 100644
index 00000000..3d176be5
--- /dev/null
+++ b/discord/assets/injection.js
@@ -0,0 +1,21 @@
+// BetterDiscord's Injection Script
+const path = require("path");
+const electron = require("electron");
+
+// Windows and macOS both use the fixed global BetterDiscord folder but
+// Electron gives the postfixed version of userData, so go up a directory
+let userConfig = path.join(electron.app.getPath("userData"), "..");
+
+// If we're on Linux there are a couple cases to deal with
+if (process.platform !== "win32" && process.platform !== "darwin") {
+ // Use || instead of ?? because a falsey value of "" is invalid per XDG spec
+ userConfig = process.env.XDG_CONFIG_HOME || path.join(process.env.HOME, ".config");
+
+ // HOST_XDG_CONFIG_HOME is set by flatpak, so use without validation if set
+ if (process.env.HOST_XDG_CONFIG_HOME) userConfig = process.env.HOST_XDG_CONFIG_HOME;
+}
+
+require(path.join(userConfig, "BetterDiscord", "data", "betterdiscord.asar"));
+
+// Discord's Default Export
+module.exports = require("./core.asar");
\ No newline at end of file
diff --git a/discord/injection.go b/discord/injection.go
new file mode 100644
index 00000000..1d7f83a4
--- /dev/null
+++ b/discord/injection.go
@@ -0,0 +1,68 @@
+package discord
+
+import (
+ _ "embed"
+ "installer/betterdiscord"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+//go:embed assets/injection.js
+var injectionScript string
+
+func (discord *DiscordInstall) inject(bd *betterdiscord.BDInstall) error {
+ if discord.IsFlatpak {
+ cmd := exec.Command("flatpak", "--user", "override", "com.discordapp."+discord.Channel.Exe(), "--filesystem="+bd.Root())
+ if err := cmd.Run(); err != nil {
+ log.Printf("❌ Could not give flatpak access to %s\n", bd.Root())
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+ }
+
+ if err := os.WriteFile(filepath.Join(discord.CorePath, "index.js"), []byte(injectionScript), 0755); err != nil {
+ log.Printf("❌ Unable to write index.js in %s\n", discord.CorePath)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ log.Printf("✅ Injected into %s\n", discord.CorePath)
+ return nil
+}
+
+func (discord *DiscordInstall) uninject() error {
+ indexFile := filepath.Join(discord.CorePath, "index.js")
+
+ contents, err := os.ReadFile(indexFile)
+
+ // First try to check the file, but if there's an issue we try to blindly overwrite below
+ if err == nil {
+ if !strings.Contains(strings.ToLower(string(contents)), "betterdiscord") {
+ log.Printf("✅ No injection found for %s\n", discord.Channel.Name())
+ return nil
+ }
+ }
+
+ if err := os.WriteFile(indexFile, []byte(`module.exports = require("./core.asar");`), 0o644); err != nil {
+ log.Printf("❌ Unable to write file %s\n", indexFile)
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+ log.Printf("✅ Removed from %s\n", discord.Channel.Name())
+
+ return nil
+}
+
+// TODO: consider putting this in the betterdiscord package
+func (discord *DiscordInstall) IsInjected() bool {
+ indexFile := filepath.Join(discord.CorePath, "index.js")
+ contents, err := os.ReadFile(indexFile)
+ if err != nil {
+ return false
+ }
+ lower := strings.ToLower(string(contents))
+ return strings.Contains(lower, "betterdiscord")
+}
diff --git a/discord/injection_test.go b/discord/injection_test.go
new file mode 100644
index 00000000..bf1a8002
--- /dev/null
+++ b/discord/injection_test.go
@@ -0,0 +1,40 @@
+package discord
+
+import (
+ "installer/types"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestIsInjected(t *testing.T) {
+ tmpDir := t.TempDir()
+ corePath := filepath.Join(tmpDir, "discord_desktop_core")
+ if err := os.MkdirAll(corePath, 0755); err != nil {
+ t.Fatalf("Failed to create core path: %v", err)
+ }
+ indexFile := filepath.Join(corePath, "index.js")
+
+ install := &DiscordInstall{
+ CorePath: corePath,
+ Channel: types.Stable,
+ }
+
+ if install.IsInjected() {
+ t.Fatalf("Expected IsInjected to be false with missing index.js")
+ }
+
+ if err := os.WriteFile(indexFile, []byte(`module.exports = require("./core.asar");`), 0644); err != nil {
+ t.Fatalf("Failed to write index.js: %v", err)
+ }
+ if install.IsInjected() {
+ t.Fatalf("Expected IsInjected to be false for default index.js")
+ }
+
+ if err := os.WriteFile(indexFile, []byte(`// BetterDiscord injected`), 0644); err != nil {
+ t.Fatalf("Failed to write injection index.js: %v", err)
+ }
+ if !install.IsInjected() {
+ t.Fatalf("Expected IsInjected to be true when BetterDiscord is present")
+ }
+}
diff --git a/discord/install.go b/discord/install.go
new file mode 100644
index 00000000..d7f79c97
--- /dev/null
+++ b/discord/install.go
@@ -0,0 +1,116 @@
+package discord
+
+import (
+ "installer/betterdiscord"
+ "installer/types"
+ "log"
+ "path/filepath"
+)
+
+type DiscordInstall struct {
+ CorePath string `json:"corePath"`
+ Channel types.DiscordChannel `json:"channel"`
+ Version string `json:"version"`
+ IsFlatpak bool `json:"isFlatpak"`
+ IsSnap bool `json:"isSnap"`
+}
+
+// InstallBD installs BetterDiscord into this Discord installation
+func (discord *DiscordInstall) InstallBD(options types.InstallOptions) error {
+ bd := discord.GetBetterDiscordInstall()
+
+ // Make BetterDiscord folders
+ log.Println("🛠 Preparing BetterDiscord...")
+ if err := bd.Prepare(); err != nil {
+ return err
+ }
+ log.Println("✅ BetterDiscord prepared for install")
+ log.Println("")
+
+ // Download and write betterdiscord.asar
+ log.Println("📥 Downloading BetterDiscord...")
+ if err := bd.Download(); err != nil {
+ return err
+ }
+ log.Println("✅ BetterDiscord downloaded")
+ log.Println("")
+
+ // Write injection script to discord_desktop_core/index.js
+ log.Println("🔌 Injecting into Discord...")
+ if err := discord.inject(bd); err != nil {
+ return err
+ }
+ log.Println("✅ Injection successful")
+ log.Println("")
+
+ if options.RestartDiscord {
+ // Terminate and restart Discord if possible
+ log.Printf("🔄 Restarting %s...\n", discord.Channel.Name())
+ if err := discord.restart(); err != nil {
+ return err
+ }
+ log.Println("")
+ }
+
+ return nil
+}
+
+// UninstallBD removes BetterDiscord from this Discord installation
+func (discord *DiscordInstall) UninstallBD(options types.UninstallOptions) error {
+ log.Println("🧹 Removing injection...")
+ if err := discord.uninject(); err != nil {
+ return err
+ }
+ log.Println("")
+
+ if options.FullUninstall {
+ install := discord.GetBetterDiscordInstall()
+ if err := install.RemoveAll(); err != nil {
+ return err
+ }
+ log.Println("")
+ }
+
+ if options.RestartDiscord {
+ log.Printf("🔄 Restarting %s...\n", discord.Channel.Name())
+ if err := discord.restart(); err != nil {
+ return err
+ }
+ log.Println("")
+ }
+
+ return nil
+}
+
+// RepairBD repairs BetterDiscord for this Discord installation
+func (discord *DiscordInstall) RepairBD(options types.RepairOptions) error {
+ if err := discord.UninstallBD(types.UninstallOptions{FullUninstall: false}); err != nil {
+ return err
+ }
+
+ // Gets the global BetterDiscord install
+ bd := betterdiscord.GetInstallation()
+
+ // Snaps get their own local BD install
+ if discord.IsSnap {
+ bd = betterdiscord.GetInstallation(filepath.Clean(filepath.Join(discord.CorePath, "..", "..", "..", "..")))
+ }
+
+ if err := bd.Repair(discord.Channel, options); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (discord *DiscordInstall) GetBetterDiscordInstall() *betterdiscord.BDInstall {
+ // Gets the global BetterDiscord install
+ bd := betterdiscord.GetInstallation()
+
+ // Snaps get their own local BD install
+ if discord.IsSnap {
+ bd = betterdiscord.GetInstallation(filepath.Clean(filepath.Join(discord.CorePath, "..", "..", "..", "..")))
+ }
+
+ return bd
+}
diff --git a/discord/paths.go b/discord/paths.go
new file mode 100644
index 00000000..4c7ee997
--- /dev/null
+++ b/discord/paths.go
@@ -0,0 +1,99 @@
+package discord
+
+import (
+ "installer/types"
+ "path/filepath"
+ "regexp"
+ "slices"
+ "strings"
+)
+
+var searchPaths []string
+var versionRegex = regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+`)
+var allDiscordInstalls map[types.DiscordChannel][]*DiscordInstall
+
+func GetAllInstalls() map[types.DiscordChannel][]*DiscordInstall {
+ allDiscordInstalls = map[types.DiscordChannel][]*DiscordInstall{}
+
+ for _, path := range searchPaths {
+ if result := Validate(path); result != nil {
+ allDiscordInstalls[result.Channel] = append(allDiscordInstalls[result.Channel], result)
+ }
+ }
+
+ sortInstalls()
+
+ return allDiscordInstalls
+}
+
+func GetVersion(proposed string) string {
+ for folder := range strings.SplitSeq(proposed, string(filepath.Separator)) {
+ if version := versionRegex.FindString(folder); version != "" {
+ return version
+ }
+ }
+ return ""
+}
+
+func GetChannel(proposed string) types.DiscordChannel {
+ for folder := range strings.SplitSeq(proposed, string(filepath.Separator)) {
+ for _, channel := range types.Channels {
+ if strings.ToLower(folder) == strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "") {
+ return channel
+ }
+ }
+ }
+ return types.Stable
+}
+
+func GetSuggestedPath(channel types.DiscordChannel) string {
+ if len(allDiscordInstalls[channel]) > 0 {
+ return allDiscordInstalls[channel][0].CorePath
+ }
+ return ""
+}
+
+func AddCustomPath(proposed string) *DiscordInstall {
+ result := Validate(proposed)
+ if result == nil {
+ return nil
+ }
+
+ // Check if this already exists in our list and return reference
+ index := slices.IndexFunc(allDiscordInstalls[result.Channel], func(d *DiscordInstall) bool { return d.CorePath == result.CorePath })
+ if index >= 0 {
+ return allDiscordInstalls[result.Channel][index]
+ }
+
+ allDiscordInstalls[result.Channel] = append(allDiscordInstalls[result.Channel], result)
+
+ sortInstalls()
+
+ return result
+}
+
+func ResolvePath(proposed string) *DiscordInstall {
+ for channel := range allDiscordInstalls {
+ index := slices.IndexFunc(allDiscordInstalls[channel], func(d *DiscordInstall) bool { return d.CorePath == proposed })
+ if index >= 0 {
+ return allDiscordInstalls[channel][index]
+ }
+ }
+
+ // If it wasn't found as an existing install, try to add it
+ return AddCustomPath(proposed)
+}
+
+func sortInstalls() {
+ for channel := range allDiscordInstalls {
+ slices.SortFunc(allDiscordInstalls[channel], func(a, b *DiscordInstall) int {
+ switch {
+ case a.Version > b.Version:
+ return -1
+ case b.Version > a.Version:
+ return 1
+ }
+ return 0
+ })
+ }
+}
diff --git a/discord/paths_common.go b/discord/paths_common.go
new file mode 100644
index 00000000..b2763cc4
--- /dev/null
+++ b/discord/paths_common.go
@@ -0,0 +1,147 @@
+//go:build darwin || linux || windows
+
+package discord
+
+import (
+ "installer/utils"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+// validateWindowsStyleInstall validates a Windows-style Discord installation path.
+// This is used for native Windows installs and WSL installs that point to Windows Discord.
+// Windows Discord has a nested structure: Discord/app-1.0.9002/modules/discord_desktop_core-1/discord_desktop_core
+func validateWindowsStyleInstall(proposed string) *DiscordInstall {
+ var finalPath = ""
+ var selected = filepath.Base(proposed)
+
+ if strings.HasPrefix(selected, "Discord") {
+ // Get version dir like app-1.0.9002
+ dFiles, err := os.ReadDir(proposed)
+ if err != nil {
+ return nil
+ }
+
+ candidates := utils.Filter(dFiles, func(file fs.DirEntry) bool {
+ return file.IsDir() && versionRegex.MatchString(file.Name())
+ })
+ if len(candidates) == 0 {
+ return nil
+ }
+ sort.Slice(candidates, func(i, j int) bool { return candidates[i].Name() < candidates[j].Name() })
+ versionDir := candidates[len(candidates)-1].Name()
+
+ // Get core wrap like discord_desktop_core-1
+ dFiles, err = os.ReadDir(filepath.Join(proposed, versionDir, "modules"))
+ if err != nil {
+ return nil
+ }
+ candidates = utils.Filter(dFiles, func(file fs.DirEntry) bool {
+ return file.IsDir() && strings.HasPrefix(file.Name(), "discord_desktop_core")
+ })
+ if len(candidates) == 0 {
+ return nil
+ }
+ coreWrap := candidates[len(candidates)-1].Name()
+
+ finalPath = filepath.Join(proposed, versionDir, "modules", coreWrap, "discord_desktop_core")
+ }
+
+ // Handle app-* directories (e.g., app-1.0.9002)
+ if strings.HasPrefix(selected, "app-") {
+ dFiles, err := os.ReadDir(filepath.Join(proposed, "modules"))
+ if err != nil {
+ return nil
+ }
+
+ candidates := utils.Filter(dFiles, func(file fs.DirEntry) bool {
+ return file.IsDir() && strings.HasPrefix(file.Name(), "discord_desktop_core")
+ })
+ if len(candidates) == 0 {
+ return nil
+ }
+ coreWrap := candidates[len(candidates)-1].Name()
+ finalPath = filepath.Join(proposed, "modules", coreWrap, "discord_desktop_core")
+ }
+
+ if selected == "discord_desktop_core" {
+ finalPath = proposed
+ }
+
+ // Verify the path and core.asar exist
+ if utils.Exists(finalPath) && utils.Exists(filepath.Join(finalPath, "core.asar")) {
+ return &DiscordInstall{
+ CorePath: finalPath,
+ Channel: GetChannel(finalPath),
+ Version: GetVersion(finalPath),
+ IsFlatpak: false,
+ IsSnap: false,
+ }
+ }
+
+ return nil
+}
+
+// validateUnixStyleInstall validates a Unix-style Discord installation path (Linux native, macOS).
+// Unix Discord has a flatter structure: discord/0.0.35/modules/discord_desktop_core
+func validateUnixStyleInstall(proposed string, detectFlatpak bool, detectSnap bool) *DiscordInstall {
+ var finalPath = ""
+ var selected = filepath.Base(proposed)
+
+ if strings.HasPrefix(strings.ToLower(selected), "discord") {
+ // Get version dir like 0.0.35
+ dFiles, err := os.ReadDir(proposed)
+ if err != nil {
+ return nil
+ }
+
+ candidates := utils.Filter(dFiles, func(file fs.DirEntry) bool {
+ return file.IsDir() && versionRegex.MatchString(file.Name())
+ })
+ if len(candidates) == 0 {
+ return nil
+ }
+ sort.Slice(candidates, func(i, j int) bool { return candidates[i].Name() < candidates[j].Name() })
+ versionDir := candidates[len(candidates)-1].Name()
+ finalPath = filepath.Join(proposed, versionDir, "modules", "discord_desktop_core")
+ }
+
+ // Handle version directories (e.g., 0.0.35)
+ if len(strings.Split(selected, ".")) == 3 {
+ finalPath = filepath.Join(proposed, "modules", "discord_desktop_core")
+ }
+
+ if selected == "modules" {
+ finalPath = filepath.Join(proposed, "discord_desktop_core")
+ }
+
+ if selected == "discord_desktop_core" {
+ finalPath = proposed
+ }
+
+ // Verify the path and core.asar exist
+ if utils.Exists(finalPath) && utils.Exists(filepath.Join(finalPath, "core.asar")) {
+ isFlatpak := false
+ isSnap := false
+
+ if detectFlatpak {
+ isFlatpak = strings.Contains(finalPath, "com.discordapp.")
+ }
+ if detectSnap {
+ isSnap = strings.Contains(finalPath, "snap/")
+ }
+
+ return &DiscordInstall{
+ CorePath: finalPath,
+ Channel: GetChannel(finalPath),
+ Version: GetVersion(finalPath),
+ IsFlatpak: isFlatpak,
+ IsSnap: isSnap,
+ }
+ }
+
+ return nil
+}
diff --git a/discord/paths_common_test.go b/discord/paths_common_test.go
new file mode 100644
index 00000000..f0bc2cfa
--- /dev/null
+++ b/discord/paths_common_test.go
@@ -0,0 +1,167 @@
+package discord
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func writeCoreAsar(t *testing.T, corePath string) {
+ t.Helper()
+ if err := os.MkdirAll(corePath, 0755); err != nil {
+ t.Fatalf("Failed to create core path: %v", err)
+ }
+ if err := os.WriteFile(filepath.Join(corePath, "core.asar"), []byte("test"), 0644); err != nil {
+ t.Fatalf("Failed to write core.asar: %v", err)
+ }
+}
+
+func TestValidateWindowsStyleInstall_FromDiscordRoot(t *testing.T) {
+ tmpDir := t.TempDir()
+ root := filepath.Join(tmpDir, "Discord")
+ versionDir := filepath.Join(root, "app-1.0.9002")
+ coreWrap := filepath.Join(versionDir, "modules", "discord_desktop_core-1", "discord_desktop_core")
+
+ writeCoreAsar(t, coreWrap)
+
+ result := validateWindowsStyleInstall(root)
+ if result == nil {
+ t.Fatalf("Expected install for %s", root)
+ }
+ if result.CorePath != coreWrap {
+ t.Errorf("CorePath = %s, expected %s", result.CorePath, coreWrap)
+ }
+}
+
+func TestValidateWindowsStyleInstall_FromAppFolder(t *testing.T) {
+ tmpDir := t.TempDir()
+ root := filepath.Join(tmpDir, "Discord")
+ versionDir := filepath.Join(root, "app-1.0.9002")
+ coreWrap := filepath.Join(versionDir, "modules", "discord_desktop_core-1", "discord_desktop_core")
+
+ writeCoreAsar(t, coreWrap)
+
+ result := validateWindowsStyleInstall(versionDir)
+ if result == nil {
+ t.Fatalf("Expected install for %s", versionDir)
+ }
+ if result.CorePath != coreWrap {
+ t.Errorf("CorePath = %s, expected %s", result.CorePath, coreWrap)
+ }
+}
+
+func TestValidateWindowsStyleInstall_FromCoreFolder(t *testing.T) {
+ tmpDir := t.TempDir()
+ corePath := filepath.Join(tmpDir, "discord_desktop_core")
+ writeCoreAsar(t, corePath)
+
+ result := validateWindowsStyleInstall(corePath)
+ if result == nil {
+ t.Fatalf("Expected install for %s", corePath)
+ }
+ if result.CorePath != corePath {
+ t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath)
+ }
+}
+
+func TestValidateWindowsStyleInstall_MissingAsar(t *testing.T) {
+ tmpDir := t.TempDir()
+ root := filepath.Join(tmpDir, "Discord")
+ versionDir := filepath.Join(root, "app-1.0.9002")
+ coreWrap := filepath.Join(versionDir, "modules", "discord_desktop_core-1", "discord_desktop_core")
+
+ if err := os.MkdirAll(coreWrap, 0755); err != nil {
+ t.Fatalf("Failed to create core path: %v", err)
+ }
+
+ result := validateWindowsStyleInstall(root)
+ if result != nil {
+ t.Fatalf("Expected no install when core.asar is missing")
+ }
+}
+
+func TestValidateUnixStyleInstall_FromDiscordRoot(t *testing.T) {
+ tmpDir := t.TempDir()
+ root := filepath.Join(tmpDir, "discord")
+ corePath := filepath.Join(root, "0.0.35", "modules", "discord_desktop_core")
+
+ writeCoreAsar(t, corePath)
+
+ result := validateUnixStyleInstall(root, true, true)
+ if result == nil {
+ t.Fatalf("Expected install for %s", root)
+ }
+ if result.CorePath != corePath {
+ t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath)
+ }
+}
+
+func TestValidateUnixStyleInstall_FromVersionFolder(t *testing.T) {
+ tmpDir := t.TempDir()
+ root := filepath.Join(tmpDir, "discord")
+ versionDir := filepath.Join(root, "0.0.35")
+ corePath := filepath.Join(versionDir, "modules", "discord_desktop_core")
+
+ writeCoreAsar(t, corePath)
+
+ result := validateUnixStyleInstall(versionDir, true, true)
+ if result == nil {
+ t.Fatalf("Expected install for %s", versionDir)
+ }
+ if result.CorePath != corePath {
+ t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath)
+ }
+}
+
+func TestValidateUnixStyleInstall_FromModulesFolder(t *testing.T) {
+ tmpDir := t.TempDir()
+ corePath := filepath.Join(tmpDir, "modules", "discord_desktop_core")
+
+ writeCoreAsar(t, corePath)
+
+ result := validateUnixStyleInstall(filepath.Join(tmpDir, "modules"), true, true)
+ if result == nil {
+ t.Fatalf("Expected install for %s", filepath.Join(tmpDir, "modules"))
+ }
+ if result.CorePath != corePath {
+ t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath)
+ }
+}
+
+func TestValidateUnixStyleInstall_FlatpakDetection(t *testing.T) {
+ tmpDir := t.TempDir()
+ root := filepath.Join(tmpDir, "com.discordapp.Discord", "config", "discord")
+ corePath := filepath.Join(root, "0.0.35", "modules", "discord_desktop_core")
+
+ writeCoreAsar(t, corePath)
+
+ result := validateUnixStyleInstall(root, true, false)
+ if result == nil {
+ t.Fatalf("Expected install for %s", root)
+ }
+ if !result.IsFlatpak {
+ t.Fatalf("Expected flatpak detection")
+ }
+ if result.IsSnap {
+ t.Fatalf("Did not expect snap detection")
+ }
+}
+
+func TestValidateUnixStyleInstall_SnapDetection(t *testing.T) {
+ tmpDir := t.TempDir()
+ root := filepath.Join(tmpDir, "snap", "discord", "current", ".config", "discord")
+ corePath := filepath.Join(root, "0.0.35", "modules", "discord_desktop_core")
+
+ writeCoreAsar(t, corePath)
+
+ result := validateUnixStyleInstall(root, false, true)
+ if result == nil {
+ t.Fatalf("Expected install for %s", root)
+ }
+ if !result.IsSnap {
+ t.Fatalf("Expected snap detection")
+ }
+ if result.IsFlatpak {
+ t.Fatalf("Did not expect flatpak detection")
+ }
+}
diff --git a/discord/paths_darwin.go b/discord/paths_darwin.go
new file mode 100644
index 00000000..7954a356
--- /dev/null
+++ b/discord/paths_darwin.go
@@ -0,0 +1,31 @@
+package discord
+
+import (
+ "installer/types"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func init() {
+ config, _ := os.UserConfigDir()
+ paths := []string{
+ filepath.Join(config, "{channel}"),
+ }
+
+ for _, channel := range types.Channels {
+ for _, path := range paths {
+ folder := strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "")
+ searchPaths = append(
+ searchPaths,
+ strings.ReplaceAll(path, "{channel}", folder),
+ )
+ }
+ }
+
+ allDiscordInstalls = GetAllInstalls()
+}
+
+func Validate(proposed string) *DiscordInstall {
+ return validateUnixStyleInstall(proposed, false, false)
+}
diff --git a/discord/paths_linux.go b/discord/paths_linux.go
new file mode 100644
index 00000000..c606c125
--- /dev/null
+++ b/discord/paths_linux.go
@@ -0,0 +1,68 @@
+package discord
+
+import (
+ "installer/types"
+ "installer/wsl"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func init() {
+ config, _ := os.UserConfigDir()
+ home, _ := os.UserHomeDir()
+ paths := []string{
+ // Native. Data is stored under `~/.config`.
+ // Example: `~/.config/discordcanary`.
+ // Core: `~/.config/discordcanary/0.0.90/modules/discord_desktop_core/core.asar`.
+ filepath.Join(config, "{channel}"),
+
+ // Flatpak. These user data paths are universal for all Flatpak installations on all machines.
+ // Example: `.var/app/com.discordapp.DiscordCanary/config/discordcanary`.
+ // Core: `.var/app/com.discordapp.DiscordCanary/config/discordcanary/0.0.90/modules/discord_desktop_core/core.asar`
+ filepath.Join(home, ".var", "app", "com.discordapp.{CHANNEL}", "config", "{channel}"),
+
+ // Snap. Just like with Flatpaks, these paths are universal for all Snap installations.
+ // Example: `snap/discord/current/.config/discord`.
+ // Example: `snap/discord-canary/current/.config/discordcanary`.
+ // Core: `snap/discord-canary/current/.config/discordcanary/0.0.90/modules/discord_desktop_core/core.asar`.
+ // NOTE: Snap user data always exists, even when the Snap isn't mounted/running.
+ filepath.Join(home, "snap", "{channel-}", "current", ".config", "{channel}"),
+ }
+
+ if wsl.IsWSL() {
+ winHome, err := wsl.WindowsHome()
+ if err == nil && winHome != "" {
+ // WSL. Data is stored under the Windows user's AppData folder.
+ // Example: `/mnt/c/Users/Username/AppData/Local/DiscordCanary`.
+ // Core: `/mnt/c/Users/Username/AppData/Local/DiscordCanary/app-1.0.9218/modules/discord_desktop_core-1/discord_desktop_core core.asar`.
+ paths = append(paths, filepath.Join(winHome, "AppData", "Local", "{CHANNEL}"))
+ }
+ }
+
+ for _, channel := range types.Channels {
+ for _, path := range paths {
+ upper := strings.ReplaceAll(channel.Name(), " ", "")
+ lower := strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "")
+ dash := strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "-")
+ folder := strings.ReplaceAll(path, "{CHANNEL}", upper)
+ folder = strings.ReplaceAll(folder, "{channel}", lower)
+ folder = strings.ReplaceAll(folder, "{channel-}", dash)
+ searchPaths = append(searchPaths, folder)
+ }
+ }
+
+ allDiscordInstalls = GetAllInstalls()
+}
+
+// Validate validates a Discord installation path on Linux.
+// For WSL environments, it uses Windows-style validation.
+// For native Linux, it detects Flatpak and Snap installations.
+func Validate(proposed string) *DiscordInstall {
+ if wsl.IsWSL() {
+ return validateWindowsStyleInstall(proposed)
+ }
+
+ // Native Linux validation with Flatpak and Snap detection
+ return validateUnixStyleInstall(proposed, true, true)
+}
diff --git a/discord/paths_test.go b/discord/paths_test.go
new file mode 100644
index 00000000..1ed26a38
--- /dev/null
+++ b/discord/paths_test.go
@@ -0,0 +1,83 @@
+package discord
+
+import (
+ "installer/types"
+ "path/filepath"
+ "testing"
+)
+
+func TestGetVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ expected string
+ }{
+ {
+ name: "Finds version in Windows style path",
+ path: filepath.Join("C:", "Users", "Me", "AppData", "Local", "Discord", "app-1.0.9002", "modules", "discord_desktop_core-1", "discord_desktop_core"),
+ expected: "1.0.9002",
+ },
+ {
+ name: "Finds version in Unix style path",
+ path: filepath.Join("/home/me/.config/discord", "0.0.35", "modules", "discord_desktop_core"),
+ expected: "0.0.35",
+ },
+ {
+ name: "No version",
+ path: filepath.Join("/home/me/.config/discord", "modules", "discord_desktop_core"),
+ expected: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := GetVersion(tt.path)
+ if result != tt.expected {
+ t.Errorf("GetVersion(%q) = %q, expected %q", tt.path, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestGetChannel(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ expected types.DiscordChannel
+ }{
+ {
+ name: "Stable path",
+ path: filepath.Join("/home/me/.config/discord", "0.0.35", "modules", "discord_desktop_core"),
+ expected: types.Stable,
+ },
+ {
+ name: "Canary path",
+ path: filepath.Join("/home/me/.config/discordcanary", "0.0.90", "modules", "discord_desktop_core"),
+ expected: types.Canary,
+ },
+ {
+ name: "PTB path",
+ path: filepath.Join("/home/me/.config/discordptb", "0.0.56", "modules", "discord_desktop_core"),
+ expected: types.PTB,
+ },
+ {
+ name: "Unknown defaults to stable",
+ path: filepath.Join("/home/me/.config/discordunknown", "0.0.56", "modules", "discord_desktop_core"),
+ expected: types.Stable,
+ },
+ {
+ name: "Windows path names",
+ path: filepath.Join("C:", "Users", "Me", "AppData", "Local", "DiscordCanary", "app-1.0.9002", "modules", "discord_desktop_core-1", "discord_desktop_core"),
+ expected: types.Canary,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := GetChannel(tt.path)
+ if result != tt.expected {
+ t.Errorf("GetChannel(%q) = %v, expected %v", tt.path, result, tt.expected)
+ }
+ })
+ }
+}
diff --git a/discord/paths_windows.go b/discord/paths_windows.go
new file mode 100644
index 00000000..12bd4b47
--- /dev/null
+++ b/discord/paths_windows.go
@@ -0,0 +1,30 @@
+package discord
+
+import (
+ "installer/types"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func init() {
+ paths := []string{
+ filepath.Join(os.Getenv("LOCALAPPDATA"), "{channel}"),
+ filepath.Join(os.Getenv("PROGRAMDATA"), os.Getenv("USERNAME"), "{channel}"),
+ }
+
+ for _, channel := range types.Channels {
+ for _, path := range paths {
+ searchPaths = append(
+ searchPaths,
+ strings.ReplaceAll(path, "{channel}", strings.ReplaceAll(channel.Name(), " ", "")),
+ )
+ }
+ }
+
+ allDiscordInstalls = GetAllInstalls()
+}
+
+func Validate(proposed string) *DiscordInstall {
+ return validateWindowsStyleInstall(proposed)
+}
diff --git a/discord/process.go b/discord/process.go
new file mode 100644
index 00000000..e8e5a9cc
--- /dev/null
+++ b/discord/process.go
@@ -0,0 +1,134 @@
+package discord
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+
+ "github.com/shirou/gopsutil/v3/process"
+)
+
+func (discord *DiscordInstall) restart() error {
+ exeName := discord.getFullExe()
+
+ if running, _ := discord.isRunning(); !running {
+ log.Printf("✅ %s is not running; skipping restart.\n", discord.Channel.Name())
+ return nil
+ }
+
+ if err := discord.kill(); err != nil {
+ log.Printf("❌ Unable to restart %s, please do so manually.\n", discord.Channel.Name())
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+
+ // Determine command based on installation type
+ var cmd *exec.Cmd
+ if discord.IsFlatpak {
+ cmd = exec.Command("flatpak", "run", "com.discordapp."+discord.Channel.Exe())
+ } else if discord.IsSnap {
+ cmd = exec.Command("snap", "run", discord.Channel.Exe())
+ } else {
+ // Use binary found in killing process for non-Flatpak/Snap installs
+ if exeName == "" {
+ log.Printf("❌ Unable to restart %s, please do so manually.\n", discord.Channel.Name())
+ return fmt.Errorf("could not determine executable path for %s", discord.Channel.Name())
+ }
+ cmd = exec.Command(exeName)
+ }
+
+ // Set working directory to user home
+ cmd.Dir, _ = os.UserHomeDir()
+
+ if err := cmd.Start(); err != nil {
+ log.Printf("❌ Unable to restart %s, please do so manually.\n", discord.Channel.Name())
+ log.Printf(" %s\n", err.Error())
+ return err
+ }
+ log.Printf("✅ Restarted %s\n", discord.Channel.Name())
+ return nil
+}
+
+func (discord *DiscordInstall) isRunning() (bool, error) {
+ name := discord.Channel.Exe()
+ processes, err := process.Processes()
+
+ // If we can't even list processes, bail out
+ if err != nil {
+ return false, fmt.Errorf("could not list processes")
+ }
+
+ // Search for desired process(es)
+ for _, p := range processes {
+ n, err := p.Name()
+
+ // Ignore processes requiring Admin/Sudo
+ if err != nil {
+ continue
+ }
+
+ // We found our target return
+ if n == name {
+ return true, nil
+ }
+ }
+
+ // If we got here, process was not found
+ return false, nil
+}
+
+func (discord *DiscordInstall) kill() error {
+ name := discord.Channel.Exe()
+ processes, err := process.Processes()
+
+ // If we can't even list processes, bail out
+ if err != nil {
+ return fmt.Errorf("could not list processes")
+ }
+
+ // Search for desired process(es)
+ for _, p := range processes {
+ n, err := p.Name()
+
+ // Ignore processes requiring Admin/Sudo
+ if err != nil {
+ continue
+ }
+
+ // We found our target, kill it
+ if n == name {
+ var killErr = p.Kill()
+
+ // We found it but can't kill it, bail out
+ if killErr != nil {
+ return killErr
+ }
+ }
+ }
+
+ // If we got here, everything was killed without error
+ return nil
+}
+
+func (discord *DiscordInstall) getFullExe() string {
+ name := discord.Channel.Exe()
+
+ var exe = ""
+ processes, err := process.Processes()
+ if err != nil {
+ return exe
+ }
+ for _, p := range processes {
+ n, err := p.Name()
+ if err != nil {
+ continue
+ }
+ if n == name {
+ if len(exe) == 0 {
+ exe, _ = p.Exe()
+ }
+ }
+ }
+ return exe
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..a774edea
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+node_modules
+
+# Output
+.output
+.vercel
+.netlify
+.wrangler
+/.svelte-kit
+/build/*
+!/build/.gitkeep
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Env
+.env
+.env.*
+!.env.example
+!.env.test
+
+# Vite
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
diff --git a/frontend/build/.gitkeep b/frontend/build/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/frontend/bun.lock b/frontend/bun.lock
new file mode 100644
index 00000000..fc738bd9
--- /dev/null
+++ b/frontend/bun.lock
@@ -0,0 +1,644 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "betterdiscord-installer",
+ "devDependencies": {
+ "@sveltejs/adapter-static": "^3.0.10",
+ "@sveltejs/kit": "^2.52.2",
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
+ "@testing-library/jest-dom": "^6.9.0",
+ "@testing-library/svelte": "^5.2.8",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/bun": "^1.3.9",
+ "@zerebos/eslint-config": "^1.0.2",
+ "@zerebos/eslint-config-svelte": "^1.0.3",
+ "@zerebos/eslint-config-typescript": "^1.0.3",
+ "eslint": "^10.0.0",
+ "focus-visible": "^5.2.1",
+ "globals": "^17.3.0",
+ "jsdom": "^26.0.0",
+ "svelte": "^5.53.0",
+ "svelte-check": "^4.4.1",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.56.0",
+ "vite": "^7.3.1",
+ "vitest": "^3.2.4",
+ },
+ },
+ },
+ "packages": {
+ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
+
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
+
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
+
+ "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.23.2", "", { "dependencies": { "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", "minimatch": "^10.2.1" } }, "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.5.2", "", { "dependencies": { "@eslint/core": "^1.1.0" } }, "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ=="],
+
+ "@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="],
+
+ "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@3.0.2", "", {}, "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.58.0", "", { "os": "android", "cpu": "arm" }, "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.58.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.58.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.58.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.58.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.58.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.58.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.58.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.58.0", "", { "os": "none", "cpu": "arm64" }, "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.58.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.58.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
+
+ "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
+
+ "@sveltejs/kit": ["@sveltejs/kit@2.53.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Brh/9h8QEg7rWIj+Nnz/2sC49NUeS8g3Qd9H5dTO3EbWG8vCEUl06jE+r5jQVDMHdr1swmCkwZkONFsWelGTpQ=="],
+
+ "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
+
+ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
+
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+
+ "@testing-library/svelte": ["@testing-library/svelte@5.3.1", "", { "dependencies": { "@testing-library/dom": "9.x.x || 10.x.x", "@testing-library/svelte-core": "1.0.0" }, "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", "vite": "*", "vitest": "*" }, "optionalPeers": ["vite", "vitest"] }, "sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w=="],
+
+ "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
+ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
+
+ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.0", "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0" } }, "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.0", "@typescript-eslint/tsconfig-utils": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="],
+
+ "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
+
+ "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
+
+ "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
+
+ "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
+
+ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
+
+ "@zerebos/eslint-config": ["@zerebos/eslint-config@1.0.2", "", { "dependencies": { "@eslint/js": "^10.0.1", "globals": "^17.3.0" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-l+rXuWCnID1VKzbH5MEXsdHvZKc+nmsAU0p9cMW7lFgATWYUhBIFOKAAw4iXh7C+WwDwoyWHEKeUB1vs6iH+iQ=="],
+
+ "@zerebos/eslint-config-svelte": ["@zerebos/eslint-config-svelte@1.0.3", "", { "dependencies": { "eslint-plugin-svelte": "^3.15.0", "typescript-eslint": "^8.56.0" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-Pf66UfeoZKcY/ECVp3rQQz9HnOZo/71ntMpDuapq+7Ghkk5W+K7BDPDn0rqHaRDhh6dl9r3sOZgleFlnsay6Rw=="],
+
+ "@zerebos/eslint-config-typescript": ["@zerebos/eslint-config-typescript@1.0.3", "", { "dependencies": { "typescript-eslint": "^8.56.0" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-nLaLb79n80ya0/zPt5gcbyAGeCZ7fzHNH1KuosV+v36P+p5PrP03ZqupUwm5OoABKRPyL5CEsY8Cro26xHmm8Q=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "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" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+ "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
+
+ "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
+
+ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
+
+ "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
+
+ "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
+
+ "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
+
+ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
+ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
+ "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="],
+
+ "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
+ "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@10.0.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.2", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.1", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ=="],
+
+ "eslint-plugin-svelte": ["eslint-plugin-svelte@3.15.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q=="],
+
+ "eslint-scope": ["eslint-scope@9.1.1", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
+ "espree": ["espree@11.1.1", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
+
+ "focus-visible": ["focus-visible@5.2.1", "", {}, "sha512-8Bx950VD1bWTQJEH/AM6SpEk+SU55aVnp4Ujhuuxy3eMEBCRwBnTBnVXr9YAPvZL3/CNjCa8u4IWfNmEO53whA=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
+
+ "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
+ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+ "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
+
+ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
+
+ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
+ "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="],
+
+ "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
+
+ "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
+
+ "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
+
+ "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
+
+ "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
+ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
+ "rollup": ["rollup@4.58.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.58.0", "@rollup/rollup-android-arm64": "4.58.0", "@rollup/rollup-darwin-arm64": "4.58.0", "@rollup/rollup-darwin-x64": "4.58.0", "@rollup/rollup-freebsd-arm64": "4.58.0", "@rollup/rollup-freebsd-x64": "4.58.0", "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", "@rollup/rollup-linux-arm-musleabihf": "4.58.0", "@rollup/rollup-linux-arm64-gnu": "4.58.0", "@rollup/rollup-linux-arm64-musl": "4.58.0", "@rollup/rollup-linux-loong64-gnu": "4.58.0", "@rollup/rollup-linux-loong64-musl": "4.58.0", "@rollup/rollup-linux-ppc64-gnu": "4.58.0", "@rollup/rollup-linux-ppc64-musl": "4.58.0", "@rollup/rollup-linux-riscv64-gnu": "4.58.0", "@rollup/rollup-linux-riscv64-musl": "4.58.0", "@rollup/rollup-linux-s390x-gnu": "4.58.0", "@rollup/rollup-linux-x64-gnu": "4.58.0", "@rollup/rollup-linux-x64-musl": "4.58.0", "@rollup/rollup-openbsd-x64": "4.58.0", "@rollup/rollup-openharmony-arm64": "4.58.0", "@rollup/rollup-win32-arm64-msvc": "4.58.0", "@rollup/rollup-win32-ia32-msvc": "4.58.0", "@rollup/rollup-win32-x64-gnu": "4.58.0", "@rollup/rollup-win32-x64-msvc": "4.58.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw=="],
+
+ "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
+
+ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
+ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
+
+ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
+
+ "svelte": ["svelte@5.53.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WzxFHZhhD23Qzu7JCYdvm1rxvRSzdt9HtHO8TScMBX51bLRFTcJmATVqjqXG+6Ln6hrViGCo9DzwOhAasxwC/w=="],
+
+ "svelte-check": ["svelte-check@4.4.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw=="],
+
+ "svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="],
+
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
+
+ "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
+
+ "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
+
+ "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
+
+ "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
+
+ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
+
+ "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
+
+ "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.56.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.0", "@typescript-eslint/parser": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
+
+ "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
+
+ "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
+
+ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
+
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
+ "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
+
+ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
+
+ "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+
+ "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
+
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
+ "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
+ "eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
+
+ "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
+
+ "svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "svelte-eslint-parser/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ }
+}
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 00000000..32c190fd
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,25 @@
+import {node} from "@zerebos/eslint-config";
+import ts from "@zerebos/eslint-config-typescript";
+import {defineConfig} from "eslint/config";
+import {build} from "@zerebos/eslint-config-svelte";
+import svelteConfig from "./svelte.config.js";
+
+
+/** @type {import("@zerebos/eslint-config-typescript").ConfigArray} */
+export default defineConfig(
+ defineConfig(
+ ...node,
+ ...ts.configs.recommendedWithTypes
+ ),
+ ...build(svelteConfig),
+ {
+ languageOptions: {
+ globals: {
+ __INSTALLER_LICENSE__: "readonly",
+ }
+ }
+ },
+ {
+ ignores: ["build/", ".svelte-kit/", "dist/", "src/lib/wailsjs/", "node_modules/"]
+ },
+);
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..aa6ed1af
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "betterdiscord-installer",
+ "productName": "BetterDiscord Installer",
+ "description": "A simple standalone program which automates the installation, removal and maintenance of BetterDiscord.",
+ "author": "BetterDiscord",
+ "private": true,
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "dev": "bun run --bun vite dev",
+ "build": "bun run --bun vite build",
+ "postbuild": "touch build/.gitkeep",
+ "preview": "bun run --bun vite preview",
+ "check": "bun run --bun svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "bun run --bun svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "lint": "bun run --bun eslint .",
+ "test": "vitest",
+ "test:watch": "vitest --watch",
+ "test:ui": "vitest --ui"
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-static": "^3.0.10",
+ "@sveltejs/kit": "^2.52.2",
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
+ "@testing-library/jest-dom": "^6.9.0",
+ "@testing-library/svelte": "^5.2.8",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/bun": "^1.3.9",
+ "@zerebos/eslint-config": "^1.0.2",
+ "@zerebos/eslint-config-svelte": "^1.0.3",
+ "@zerebos/eslint-config-typescript": "^1.0.3",
+ "eslint": "^10.0.0",
+ "focus-visible": "^5.2.1",
+ "globals": "^17.3.0",
+ "jsdom": "^26.0.0",
+ "svelte": "^5.53.0",
+ "svelte-check": "^4.4.1",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.56.0",
+ "vite": "^7.3.1",
+ "vitest": "^3.2.4"
+ }
+}
diff --git a/frontend/package.json.md5 b/frontend/package.json.md5
new file mode 100644
index 00000000..bf0f6694
--- /dev/null
+++ b/frontend/package.json.md5
@@ -0,0 +1 @@
+433b24fd52063a0df29a5d5a99ac0fdb
\ No newline at end of file
diff --git a/frontend/src/app.html b/frontend/src/app.html
new file mode 100644
index 00000000..5889edbd
--- /dev/null
+++ b/frontend/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+
%sveltekit.body%
+
+
diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts
new file mode 100644
index 00000000..78d22928
--- /dev/null
+++ b/frontend/src/env.d.ts
@@ -0,0 +1 @@
+declare const __INSTALLER_LICENSE__: string;
\ No newline at end of file
diff --git a/assets/images/background.png b/frontend/src/lib/assets/images/background.png
similarity index 100%
rename from assets/images/background.png
rename to frontend/src/lib/assets/images/background.png
diff --git a/assets/images/canary.png b/frontend/src/lib/assets/images/canary.png
similarity index 100%
rename from assets/images/canary.png
rename to frontend/src/lib/assets/images/canary.png
diff --git a/assets/images/ptb.png b/frontend/src/lib/assets/images/ptb.png
similarity index 100%
rename from assets/images/ptb.png
rename to frontend/src/lib/assets/images/ptb.png
diff --git a/assets/images/stable.png b/frontend/src/lib/assets/images/stable.png
similarity index 100%
rename from assets/images/stable.png
rename to frontend/src/lib/assets/images/stable.png
diff --git a/assets/license.txt b/frontend/src/lib/assets/license.txt
similarity index 100%
rename from assets/license.txt
rename to frontend/src/lib/assets/license.txt
diff --git a/src/renderer/common/Button.svelte b/frontend/src/lib/components/Button.svelte
similarity index 56%
rename from src/renderer/common/Button.svelte
rename to frontend/src/lib/components/Button.svelte
index 88f6fccc..ed9262de 100644
--- a/src/renderer/common/Button.svelte
+++ b/frontend/src/lib/components/Button.svelte
@@ -1,18 +1,30 @@
-
@@ -45,23 +57,23 @@
text-overflow: ellipsis;
}
- .button.type-primary {
+ .button.style-primary {
border: 1px solid transparent;
background-color: var(--accent);
color: #ffffff;
}
- .button.type-primary:hover {
+ .button.style-primary:hover {
background-color: var(--accent-hover);
}
- .button.type-secondary {
+ .button.style-secondary {
background-color: transparent;
border: 1px solid rgba(255, 255, 255, 0.05);
color: var(--text-normal);
}
- .button.type-secondary:hover {
+ .button.style-secondary:hover {
border-color: rgba(255, 255, 255, 0.1);
}
\ No newline at end of file
diff --git a/frontend/src/lib/components/Button.test.ts b/frontend/src/lib/components/Button.test.ts
new file mode 100644
index 00000000..1f2fcfba
--- /dev/null
+++ b/frontend/src/lib/components/Button.test.ts
@@ -0,0 +1,36 @@
+import {createRawSnippet} from "svelte";
+import {expect, it, describe} from "vitest";
+import {render} from "@testing-library/svelte";
+import Button from "./Button.svelte";
+
+
+describe("Button", () => {
+ it("renders content inside the button", () => {
+ const label = createRawSnippet(() => ({
+ render: () => "Click me"
+ }));
+
+ const {getByRole} = render(Button, {
+ props: {
+ children: label
+ }
+ });
+
+ expect(getByRole("button")).toHaveTextContent("Click me");
+ });
+
+ it("uses the primary style when requested", () => {
+ const label = createRawSnippet(() => ({
+ render: () => "Primary"
+ }));
+
+ const {getByRole} = render(Button, {
+ props: {
+ style: "primary",
+ children: label
+ }
+ });
+
+ expect(getByRole("button")).toHaveClass("style-primary");
+ });
+});
diff --git a/src/renderer/common/ButtonGroup.svelte b/frontend/src/lib/components/ButtonGroup.svelte
similarity index 60%
rename from src/renderer/common/ButtonGroup.svelte
rename to frontend/src/lib/components/ButtonGroup.svelte
index 62c44753..349dfd31 100644
--- a/src/renderer/common/ButtonGroup.svelte
+++ b/frontend/src/lib/components/ButtonGroup.svelte
@@ -1,7 +1,15 @@
-
-
+
+
+
+
+ {@render children()}
+
\ No newline at end of file
diff --git a/src/renderer/pages/Loading.svelte b/frontend/src/lib/components/Loader.svelte
similarity index 71%
rename from src/renderer/pages/Loading.svelte
rename to frontend/src/lib/components/Loader.svelte
index fea6185f..970b2918 100644
--- a/src/renderer/pages/Loading.svelte
+++ b/frontend/src/lib/components/Loader.svelte
@@ -1,9 +1,9 @@
-
-
+
\ No newline at end of file
diff --git a/frontend/src/lib/components/Page.svelte b/frontend/src/lib/components/Page.svelte
new file mode 100644
index 00000000..20cf3009
--- /dev/null
+++ b/frontend/src/lib/components/Page.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/common/PageHeader.svelte b/frontend/src/lib/components/PageHeader.svelte
similarity index 85%
rename from src/renderer/common/PageHeader.svelte
rename to frontend/src/lib/components/PageHeader.svelte
index 2501cf18..07e83fe9 100644
--- a/src/renderer/common/PageHeader.svelte
+++ b/frontend/src/lib/components/PageHeader.svelte
@@ -1,22 +1,32 @@
-