├── decky_plugin ├── py_modules │ └── .keep ├── assets │ └── logo.png ├── backend │ ├── src │ │ └── main.c │ ├── entrypoint.sh │ ├── Makefile │ └── Dockerfile ├── rollup.config.js ├── src │ ├── types.d.ts │ └── index.tsx ├── plugin.json ├── tsconfig.json ├── .gitignore ├── defaults │ └── defaults.txt ├── package.json ├── LICENSE ├── decky.pyi ├── README.md ├── main.py └── pnpm-lock.yaml ├── assets ├── icons │ └── romm_icon.png └── screenshots │ └── main_interface.png ├── requirements.txt ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── README.md ├── scripts ├── clean_build.sh └── setup_dev.sh ├── romm_platform_slugs.json ├── romm_platform_slugs.py └── src └── bios_manager.py /decky_plugin/py_modules/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/romm_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Covin90/romm-retroarch-sync/HEAD/assets/icons/romm_icon.png -------------------------------------------------------------------------------- /decky_plugin/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Covin90/romm-retroarch-sync/HEAD/decky_plugin/assets/logo.png -------------------------------------------------------------------------------- /decky_plugin/backend/src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | int main() { 3 | printf("Hello World\n"); 4 | return 0; 5 | } -------------------------------------------------------------------------------- /assets/screenshots/main_interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Covin90/romm-retroarch-sync/HEAD/assets/screenshots/main_interface.png -------------------------------------------------------------------------------- /decky_plugin/rollup.config.js: -------------------------------------------------------------------------------- 1 | import deckyPlugin from "@decky/rollup"; 2 | 3 | export default deckyPlugin({ 4 | // Add your extra Rollup options here 5 | }) -------------------------------------------------------------------------------- /decky_plugin/backend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "Container's IP address: `awk 'END{print $1}' /etc/hosts`" 5 | 6 | cd /backend 7 | 8 | make -------------------------------------------------------------------------------- /decky_plugin/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.png" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.jpg" { 12 | const content: string; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core dependencies for RomM-RetroArch Sync 2 | requests>=2.28.0 3 | watchdog>=3.0.0 4 | cryptography>=3.4.8 5 | 6 | # GTK4 and GObject bindings (system-provided, listed for reference) 7 | # PyGObject>=3.42.0 8 | 9 | # Optional: Development and testing dependencies 10 | # pytest>=7.0.0 11 | # flake8>=4.0.0 -------------------------------------------------------------------------------- /decky_plugin/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RomM Sync Monitor", 3 | "author": "Your Name", 4 | "flags": ["_root"], 5 | "api_version": 1, 6 | "publish": { 7 | "tags": ["utility", "romm"], 8 | "description": "Monitor RomM-RetroArch Sync service status", 9 | "image": "https://opengraph.githubassets.com/1/SteamDeckHomebrew/PluginLoader" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /decky_plugin/backend/Makefile: -------------------------------------------------------------------------------- 1 | # This is the default target, which will be built when 2 | # you invoke make 3 | .PHONY: all 4 | all: hello 5 | 6 | # This rule tells make how to build hello from hello.cpp 7 | hello: 8 | mkdir -p ./out 9 | gcc -o ./out/hello ./src/main.c 10 | 11 | # This rule tells make to delete hello and hello.o 12 | .PHONY: clean 13 | clean: 14 | rm -f hello -------------------------------------------------------------------------------- /decky_plugin/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # we support images for building with a vanilla SteamOS base, 2 | # or versions with ootb support for rust or go 3 | # developers can also customize these images via this Dockerfile 4 | #FROM ghcr.io/steamdeckhomebrew/holo-toolchain-rust:latest 5 | #FROM ghcr.io/steamdeckhomebrew/holo-toolchain-go:latest 6 | FROM ghcr.io/steamdeckhomebrew/holo-base:latest 7 | 8 | # entrypoint.sh should always be located in the backend folder 9 | ENTRYPOINT [ "/backend/entrypoint.sh" ] -------------------------------------------------------------------------------- /decky_plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "ESNext", 5 | "target": "ES2020", 6 | "jsx": "react", 7 | "jsxFactory": "window.SP_REACT.createElement", 8 | "jsxFragmentFactory": "window.SP_REACT.Fragment", 9 | "declaration": false, 10 | "moduleResolution": "node", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strict": true, 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "include": ["src"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /decky_plugin/.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Coverage reports 17 | coverage 18 | 19 | # API keys and secrets 20 | .env 21 | 22 | # Dependency directory 23 | node_modules 24 | bower_components 25 | .pnpm-store 26 | 27 | # Editors 28 | .idea 29 | *.iml 30 | 31 | # OS metadata 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Ignore built ts files 36 | dist/ 37 | 38 | __pycache__/ 39 | 40 | /.yalc 41 | yalc.lock 42 | 43 | .vscode/settings.json 44 | 45 | # Ignore output folder 46 | 47 | backend/out 48 | 49 | # Make sure to ignore any instance of the loader's decky_plugin.py 50 | decky_plugin.py 51 | 52 | # Ignore decky CLI for building plugins 53 | out 54 | out/* 55 | cli/ 56 | cli/* 57 | cli/decky 58 | -------------------------------------------------------------------------------- /decky_plugin/defaults/defaults.txt: -------------------------------------------------------------------------------- 1 | If you have plain-text json configs, theme templates, or templates for usage for your plugin of any description you should have those files be in here. 2 | Those files will be pulled into the zip during the build process and included with the upload. Example: CssLoader with it's themes in "default/themes" would have the "themes" folder will be added alongside with the dist folder, main.py, LICENSE and README files in the subfolder of the zip containing the plugin. 3 | Files can also be put in here such as a config, just keep in mind that they this directory cannot be utilized to put files in arbitrary locations, just within the extracted root folder of the plugin, ex: CssLoader has "defaults/themes/..." setup in it's repo, but when packaged to go to the store, the file structure will be: 4 | 5 | - LICENSE 6 | - README 7 | - dist 8 | - index.js 9 | - main.py 10 | - package.json 11 | - plugin.json 12 | - themes 13 | - exampletheme.css -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/AppDir/ 8 | build/RomM-*.AppImage 9 | build/*.zsync 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # Virtual environments 27 | venv/ 28 | env/ 29 | ENV/ 30 | .venv/ 31 | 32 | # IDE and editors 33 | .vscode/ 34 | .idea/ 35 | *.swp 36 | *.swo 37 | *~ 38 | .vs/ 39 | 40 | # OS generated files 41 | .DS_Store 42 | .DS_Store? 43 | ._* 44 | .Spotlight-V100 45 | .Trashes 46 | ehthumbs.db 47 | Thumbs.db 48 | desktop.ini 49 | 50 | # Application specific 51 | *.log 52 | config.ini 53 | cache/ 54 | temp/ 55 | 56 | # Build artifacts 57 | AppDir/ 58 | *.AppImage 59 | *.zsync 60 | appimagetool* 61 | 62 | # Screenshots (except assets) 63 | screenshot_*.png 64 | screen_*.png 65 | 66 | # Temporary files 67 | *.tmp 68 | *.temp 69 | .cache/ 70 | 71 | # User configuration and data 72 | .config/ 73 | user_data/ 74 | local_settings.ini 75 | 76 | # Development tools 77 | .pytest_cache/ 78 | .coverage 79 | htmlcov/ 80 | .tox/ 81 | .mypy_cache/ 82 | .dmypy.json 83 | dmypy.json 84 | 85 | # Distribution and packaging 86 | *.deb 87 | *.rpm 88 | *.tar.gz 89 | *.zip 90 | releases/ 91 | -------------------------------------------------------------------------------- /decky_plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "romm-sync-monitor", 3 | "version": "1.0.0", 4 | "description": "RomM Sync Service Monitor", 5 | "type": "module", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "watch": "rollup -c -w", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/SteamDeckHomebrew/decky-plugin-template.git" 14 | }, 15 | "keywords": [ 16 | "decky", 17 | "plugin", 18 | "plugin-template", 19 | "steam-deck", 20 | "deck" 21 | ], 22 | "author": "You ", 23 | "license": "BSD-3-Clause", 24 | "bugs": { 25 | "url": "https://github.com/SteamDeckHomebrew/decky-plugin-template/issues" 26 | }, 27 | "homepage": "https://github.com/SteamDeckHomebrew/decky-plugin-template#readme", 28 | "devDependencies": { 29 | "@decky/rollup": "^1.0.1", 30 | "@decky/ui": "^4.7.2", 31 | "@types/react": "18.3.3", 32 | "@types/react-dom": "18.3.0", 33 | "@types/webpack": "^5.28.5", 34 | "rollup": "^4.22.5", 35 | "typescript": "^5.6.2" 36 | }, 37 | "dependencies": { 38 | "@decky/api": "^1.1.2", 39 | "react-icons": "^5.3.0", 40 | "tslib": "^2.7.0" 41 | }, 42 | "pnpm": { 43 | "peerDependencyRules": { 44 | "ignoreMissing": [ 45 | "react", 46 | "react-dom" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /decky_plugin/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Hypothetical Plugin Developer 4 | Original Copyright (c) 2022-2024, Steam Deck Homebrew 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | 3. Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2025 RomM-RetroArch Sync Contributors 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | ================================================================================ 20 | 21 | GNU GENERAL PUBLIC LICENSE 22 | Version 3, 29 June 2007 23 | 24 | Copyright (C) 2007 Free Software Foundation, Inc. 25 | Everyone is permitted to copy and distribute verbatim copies 26 | of this license document, but changing it is not allowed. 27 | 28 | Preamble 29 | 30 | The GNU General Public License is a free, copyleft license for 31 | software and other kinds of works. 32 | 33 | The licenses for most software and other practical works are designed 34 | to take away your freedom to share and change the works. By contrast, 35 | the GNU General Public License is intended to guarantee your freedom to 36 | share and change all versions of a program--to make sure it remains free 37 | software for all its users. We, the Free Software Foundation, use the 38 | GNU General Public License for most of our software; it applies also to 39 | any other work released this way by its authors. You can apply it to 40 | your programs, too. 41 | 42 | [Full GPL-3.0 license text continues...] 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.3] - 2025-11-16 4 | 5 | ### Tweaked 6 | 7 | - Status display and tree view improvements 8 | 9 | ### Fixed 10 | 11 | - Bulk cancel operation behavior 12 | - Size calculation and sync bugs 13 | 14 | 15 | ## [1.2.1] - 2025-10-19 16 | 17 | ### Added 18 | 19 | - System \ Retroarch collection sync notification 20 | 21 | ### Fixed 22 | 23 | - Multilple ui bugs in tree view 24 | 25 | ## [1.2] - 2025-10-19 26 | 27 | ### Added 28 | 29 | - RomM Collection Sync 30 | - BIOS Automatic Download 31 | 32 | ### Fixed 33 | 34 | - Multilple files game 35 | - Compatibility with latest RomM (4.1.0+) 36 | - Retrodeck compatibility 37 | - Various fixes to tree view (first batch) 38 | 39 | ## [1.1.0] - 2025-08-06 40 | 41 | ### Added 42 | 43 | - Steam Deck & RetroDeck compatibility 44 | - Max concurrent downloads 45 | - Daemon service 46 | - Tweaked 47 | - Game detection & core matching algorithms 48 | - UI & Notifications system 49 | 50 | ## [1.0.6] - 2025-07-27 51 | 52 | ### Added 53 | - Library caching system 54 | 55 | ### Tweaked 56 | - Reduced memory consumption 57 | - Tweaked fetching algorithm 58 | 59 | ## [1.0.5] - 2025-07-26 60 | 61 | ### Added 62 | - Sorting by Alphabetical / Downloaded 63 | - Showing All / Downloaded only 64 | - Parallelized fetching algorithm 65 | 66 | ### Tweaked 67 | - Improved visual feedback while loading library 68 | 69 | ## [1.0.3] - 2025-07-26 70 | 71 | ### Fixed 72 | - Fixed again RomM API pagination 73 | - Fixed save state sync behaviour 74 | 75 | ### Added 76 | - Added visual feedback while loading library 77 | 78 | ## [1.0.1] - 2025-07-25 79 | 80 | ### Fixed 81 | - Fixed RomM API pagination - now loads ALL games from large libraries instead of just the first ~50 games 82 | 83 | ## [1.0.0] - 2025-07-13 84 | 85 | ### Added 86 | - RomM server connection and authentication 87 | - Game library browsing and management 88 | - ROM downloading with progress tracking 89 | - RetroArch integration and game launching 90 | - Save file and save state synchronization 91 | - Auto-sync functionality with file monitoring 92 | - Bulk download and delete operations 93 | - Game search and filtering 94 | - Tree view game library interface 95 | - System tray integration 96 | - AppImage build system 97 | - GTK4/Adwaita UI design -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RomM - RetroArch Sync 2 | 3 | A desktop application for managing your retro game library by syncing ROMs, saves, and save states between RomM server and RetroArch. 4 | 5 | ![RomM-RetroArch Sync](assets/screenshots/main_interface.png) 6 | 7 | ## 📥 Download 8 | 9 |
10 | 11 | [![Download](https://img.shields.io/github/v/release/Covin90/romm-retroarch-sync?style=for-the-badge&logo=github&label=Download%20v1.0.6)](https://github.com/Covin90/romm-retroarch-sync/releases/download/v1.0.6/RomM-RetroArch-Sync-v1.0.6.AppImage) 12 | 13 | **[All Releases](https://github.com/Covin90/romm-retroarch-sync/releases)** • **[Issues](https://github.com/Covin90/romm-retroarch-sync/issues)** 14 | 15 |
16 | 17 | ## ✨ Features 18 | 19 | - **🎮 Game Library Management**: Browse and download your entire RomM game collection 20 | - **🔄 Auto-Sync**: Automatically sync save files and save states between RetroArch and RomM 21 | - **🚀 RetroArch Integration**: Launch games directly with appropriate cores 22 | 23 | ## 🚀 Quick Start 24 | 25 | ### Download AppImage (Recommended) 26 | 27 | 1. Download the latest `RomM-RetroArch-Sync.AppImage` from [Releases](../../releases) 28 | 2. Make it executable: `chmod +x RomM-RetroArch-Sync.AppImage` 29 | 3. Run: `./RomM-RetroArch-Sync.AppImage` 30 | 31 | ## 🔧 Configuration 32 | 33 | 1. **Connect to RomM**: Enter your RomM server URL and credentials 34 | 2. **Verify RetroArch**: Check that RetroArch installation is detected 35 | 3. **Set Download Path**: Choose where to store downloaded ROMs 36 | 4. **Enable Auto-Sync**: Turn on automatic save file synchronization 37 | 38 | ## 🔗 Links 39 | 40 | - **RomM Project**: [GitHub](https://github.com/rommapp/romm) 41 | - **RetroArch**: [Official Site](https://www.retroarch.com/) 42 | 43 | ## 📜 License 44 | 45 | This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. 46 | 47 | ## 🤝 Contributing 48 | 49 | Contributions are welcome! Please feel free to submit a Pull Request. 50 | 51 | ## 🐛 Bug Reports 52 | 53 | Found a bug? Please [open an issue](../../issues) with details about your system and the problem. 54 | 55 | --- 56 | 57 | **Note**: This application requires a running RomM server and is designed for personal game library management. Please ensure you own the games you are downloading and managing. 58 | -------------------------------------------------------------------------------- /decky_plugin/decky.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | This module exposes various constants and helpers useful for decky plugins. 3 | 4 | * Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`. 5 | * Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`. 6 | * Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`. 7 | 8 | Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended. 9 | 10 | Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`. 11 | 12 | A logging facility `logger` is available which writes to the recommended location. 13 | """ 14 | 15 | __version__ = '1.0.0' 16 | 17 | import logging 18 | 19 | from typing import Any 20 | 21 | """ 22 | Constants 23 | """ 24 | 25 | HOME: str 26 | """ 27 | The home directory of the effective user running the process. 28 | Environment variable: `HOME`. 29 | If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in. 30 | e.g.: `/home/deck` 31 | """ 32 | 33 | USER: str 34 | """ 35 | The effective username running the process. 36 | Environment variable: `USER`. 37 | It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in. 38 | e.g.: `deck` 39 | """ 40 | 41 | DECKY_VERSION: str 42 | """ 43 | The version of the decky loader. 44 | Environment variable: `DECKY_VERSION`. 45 | e.g.: `v2.5.0-pre1` 46 | """ 47 | 48 | DECKY_USER: str 49 | """ 50 | The user whose home decky resides in. 51 | Environment variable: `DECKY_USER`. 52 | e.g.: `deck` 53 | """ 54 | 55 | DECKY_USER_HOME: str 56 | """ 57 | The home of the user where decky resides in. 58 | Environment variable: `DECKY_USER_HOME`. 59 | e.g.: `/home/deck` 60 | """ 61 | 62 | DECKY_HOME: str 63 | """ 64 | The root of the decky folder. 65 | Environment variable: `DECKY_HOME`. 66 | e.g.: `/home/deck/homebrew` 67 | """ 68 | 69 | DECKY_PLUGIN_SETTINGS_DIR: str 70 | """ 71 | The recommended path in which to store configuration files (created automatically). 72 | Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`. 73 | e.g.: `/home/deck/homebrew/settings/decky-plugin-template` 74 | """ 75 | 76 | DECKY_PLUGIN_RUNTIME_DIR: str 77 | """ 78 | The recommended path in which to store runtime data (created automatically). 79 | Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`. 80 | e.g.: `/home/deck/homebrew/data/decky-plugin-template` 81 | """ 82 | 83 | DECKY_PLUGIN_LOG_DIR: str 84 | """ 85 | The recommended path in which to store persistent logs (created automatically). 86 | Environment variable: `DECKY_PLUGIN_LOG_DIR`. 87 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template` 88 | """ 89 | 90 | DECKY_PLUGIN_DIR: str 91 | """ 92 | The root of the plugin's directory. 93 | Environment variable: `DECKY_PLUGIN_DIR`. 94 | e.g.: `/home/deck/homebrew/plugins/decky-plugin-template` 95 | """ 96 | 97 | DECKY_PLUGIN_NAME: str 98 | """ 99 | The name of the plugin as specified in the 'plugin.json'. 100 | Environment variable: `DECKY_PLUGIN_NAME`. 101 | e.g.: `Example Plugin` 102 | """ 103 | 104 | DECKY_PLUGIN_VERSION: str 105 | """ 106 | The version of the plugin as specified in the 'package.json'. 107 | Environment variable: `DECKY_PLUGIN_VERSION`. 108 | e.g.: `0.0.1` 109 | """ 110 | 111 | DECKY_PLUGIN_AUTHOR: str 112 | """ 113 | The author of the plugin as specified in the 'plugin.json'. 114 | Environment variable: `DECKY_PLUGIN_AUTHOR`. 115 | e.g.: `John Doe` 116 | """ 117 | 118 | DECKY_PLUGIN_LOG: str 119 | """ 120 | The path to the plugin's main logfile. 121 | Environment variable: `DECKY_PLUGIN_LOG`. 122 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log` 123 | """ 124 | 125 | """ 126 | Migration helpers 127 | """ 128 | 129 | 130 | def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]: 131 | """ 132 | Migrate files and directories to a new location and remove old locations. 133 | Specified files will be migrated to `target_dir`. 134 | Specified directories will have their contents recursively migrated to `target_dir`. 135 | 136 | Returns the mapping of old -> new location. 137 | """ 138 | 139 | 140 | def migrate_settings(*files_or_directories: str) -> dict[str, str]: 141 | """ 142 | Migrate files and directories relating to plugin settings to the recommended location and remove old locations. 143 | Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 144 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 145 | 146 | Returns the mapping of old -> new location. 147 | """ 148 | 149 | 150 | def migrate_runtime(*files_or_directories: str) -> dict[str, str]: 151 | """ 152 | Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations 153 | Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 154 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 155 | 156 | Returns the mapping of old -> new location. 157 | """ 158 | 159 | 160 | def migrate_logs(*files_or_directories: str) -> dict[str, str]: 161 | """ 162 | Migrate files and directories relating to plugin logs to the recommended location and remove old locations. 163 | Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`. 164 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`. 165 | 166 | Returns the mapping of old -> new location. 167 | """ 168 | 169 | 170 | """ 171 | Logging 172 | """ 173 | 174 | logger: logging.Logger 175 | """The main plugin logger writing to `DECKY_PLUGIN_LOG`.""" 176 | 177 | """ 178 | Event handling 179 | """ 180 | # TODO better docstring im lazy 181 | async def emit(event: str, *args: Any) -> None: 182 | """ 183 | Send an event to the frontend. 184 | """ -------------------------------------------------------------------------------- /decky_plugin/README.md: -------------------------------------------------------------------------------- 1 | # Decky Plugin Template [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://deckbrew.xyz/discord) 2 | 3 | Reference example for using [decky-frontend-lib](https://github.com/SteamDeckHomebrew/decky-frontend-lib) (@decky/ui) in a [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader) plugin. 4 | 5 | ### **Please also refer to the [wiki](https://wiki.deckbrew.xyz/en/user-guide/home#plugin-development) for important information on plugin development and submissions/updates. currently documentation is split between this README and the wiki which is something we are hoping to rectify in the future.** 6 | 7 | ## Developers 8 | 9 | ### Dependencies 10 | 11 | This template relies on the user having Node.js v16.14+ and `pnpm` (v9) installed on their system. 12 | Please make sure to install pnpm v9 to prevent issues with CI during plugin submission. 13 | `pnpm` can be downloaded from `npm` itself which is recommended. 14 | 15 | #### Linux 16 | 17 | ```bash 18 | sudo npm i -g pnpm@9 19 | ``` 20 | 21 | If you would like to build plugins that have their own custom backends, Docker is required as it is used by the Decky CLI tool. 22 | 23 | ### Making your own plugin 24 | 25 | 1. You can fork this repo or utilize the "Use this template" button on Github. 26 | 2. In your local fork/own plugin-repository run these commands: 27 | 1. ``pnpm i`` 28 | 2. ``pnpm run build`` 29 | - These setup pnpm and build the frontend code for testing. 30 | 3. Consult the [decky-frontend-lib](https://github.com/SteamDeckHomebrew/decky-frontend-lib) repository for ways to accomplish your tasks. 31 | - Documentation and examples are still rough, 32 | - Decky loader primarily targets Steam Deck hardware so keep this in mind when developing your plugin. 33 | 4. If using VSCodium/VSCode, run the `setup` and `build` and `deploy` tasks. If not using VSCodium etc. you can derive your own makefile or just manually utilize the scripts for these commands as you see fit. 34 | 35 | If you use VSCode or it's derivatives (we suggest [VSCodium](https://vscodium.com/)!) just run the `setup` and `build` tasks. It's really that simple. 36 | 37 | #### Other important information 38 | 39 | Everytime you change the frontend code (`index.tsx` etc) you will need to rebuild using the commands from step 2 above or the build task if you're using vscode or a derivative. 40 | 41 | Note: If you are receiving build errors due to an out of date library, you should run this command inside of your repository: 42 | 43 | ```bash 44 | pnpm update @decky/ui --latest 45 | ``` 46 | 47 | ### Backend support 48 | 49 | If you are developing with a backend for a plugin and would like to submit it to the [decky-plugin-database](https://github.com/SteamDeckHomebrew/decky-plugin-database) you will need to have all backend code located in ``backend/src``, with backend being located in the root of your git repository. 50 | When building your plugin, the source code will be built and any finished binary or binaries will be output to ``backend/out`` (which is created during CI.) 51 | If your buildscript, makefile or any other build method does not place the binary files in the ``backend/out`` directory they will not be properly picked up during CI and your plugin will not have the required binaries included for distribution. 52 | 53 | Example: 54 | In our makefile used to demonstrate the CI process of building and distributing a plugin backend, note that the makefile explicitly creates the `out` folder (``backend/out``) and then compiles the binary into that folder. Here's the relevant snippet. 55 | 56 | ```make 57 | hello: 58 | mkdir -p ./out 59 | gcc -o ./out/hello ./src/main.c 60 | ``` 61 | 62 | The CI does create the `out` folder itself but we recommend creating it yourself if possible during your build process to ensure the build process goes smoothly. 63 | 64 | Note: When locally building your plugin it will be placed into a folder called 'out' this is different from the concept described above. 65 | 66 | The out folder is not sent to the final plugin, but is then put into a ``bin`` folder which is found at the root of the plugin's directory. 67 | More information on the bin folder can be found below in the distribution section below. 68 | 69 | ### Distribution 70 | 71 | We recommend following the instructions found in the [decky-plugin-database](https://github.com/SteamDeckHomebrew/decky-plugin-database) on how to get your plugin up on the plugin store. This is the best way to get your plugin in front of users. 72 | You can also choose to do distribution via a zip file containing the needed files, if that zip file is uploaded to a URL it can then be downloaded and installed via decky-loader. 73 | 74 | **NOTE: We do not currently have a method to install from a downloaded zip file in "game-mode" due to lack of a usable file-picking dialog.** 75 | 76 | Layout of a plugin zip ready for distribution: 77 | ``` 78 | pluginname-v1.0.0.zip (version number is optional but recommended for users sake) 79 | | 80 | pluginname/ 81 | | | | 82 | | | bin/ (optional) 83 | | | | 84 | | | binary (optional) 85 | | | 86 | | dist/ [required] 87 | | | 88 | | index.js [required] 89 | | 90 | package.json [required] 91 | plugin.json [required] 92 | main.py {required if you are using the python backend of decky-loader: serverAPI} 93 | README.md (optional but recommended) 94 | LICENSE(.md) [required, filename should be roughly similar, suffix not needed] 95 | ``` 96 | 97 | Note regarding licenses: Including a license is required for the plugin store if your chosen license requires the license to be included alongside usage of source-code/binaries! 98 | 99 | Standard procedure for licenses is to have your chosen license at the top of the file, and to leave the original license for the plugin-template at the bottom. If this is not the case on submission to the plugin database, you will be asked to fix this discrepancy. 100 | 101 | We cannot and will not distribute your plugin on the Plugin Store if it's license requires it's inclusion but you have not included a license to be re-distributed with your plugin in the root of your git repository. 102 | -------------------------------------------------------------------------------- /scripts/clean_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # RomM-RetroArch Sync - Clean Build Script 4 | # Removes all build artifacts, temporary files, and generated content 5 | 6 | echo "🧹 RomM-RetroArch Sync - Clean Build" 7 | echo "==================================" 8 | echo "" 9 | 10 | # Colors for output 11 | RED='\033[0;31m' 12 | GREEN='\033[0;32m' 13 | YELLOW='\033[1;33m' 14 | BLUE='\033[0;34m' 15 | NC='\033[0m' # No Color 16 | 17 | # Helper functions 18 | print_status() { 19 | echo -e "${BLUE}ℹ️ $1${NC}" 20 | } 21 | 22 | print_success() { 23 | echo -e "${GREEN}✅ $1${NC}" 24 | } 25 | 26 | print_warning() { 27 | echo -e "${YELLOW}⚠️ $1${NC}" 28 | } 29 | 30 | print_error() { 31 | echo -e "${RED}❌ $1${NC}" 32 | } 33 | 34 | # Check if script is run from project root 35 | if [ ! -f "src/romm_sync_app.py" ]; then 36 | print_error "Please run this script from the project root directory" 37 | exit 1 38 | fi 39 | 40 | print_status "Cleaning build artifacts and temporary files..." 41 | echo "" 42 | 43 | # Track what we're cleaning 44 | CLEANED_ITEMS=0 45 | CLEANED_SIZE=0 46 | 47 | # Function to remove files/directories and track them 48 | clean_item() { 49 | local item="$1" 50 | local description="$2" 51 | 52 | if [ -e "$item" ]; then 53 | # Calculate size before deletion (if it's a file) 54 | if [ -f "$item" ]; then 55 | local size=$(du -sb "$item" 2>/dev/null | cut -f1 || echo 0) 56 | CLEANED_SIZE=$((CLEANED_SIZE + size)) 57 | elif [ -d "$item" ]; then 58 | local size=$(du -sb "$item" 2>/dev/null | cut -f1 || echo 0) 59 | CLEANED_SIZE=$((CLEANED_SIZE + size)) 60 | fi 61 | 62 | rm -rf "$item" 63 | print_success "Removed $description" 64 | CLEANED_ITEMS=$((CLEANED_ITEMS + 1)) 65 | fi 66 | } 67 | 68 | # 1. AppImage build artifacts 69 | print_status "Cleaning AppImage build artifacts..." 70 | clean_item "build/AppDir" "AppImage build directory" 71 | clean_item "build/RomM-RetroArch-Sync*.AppImage" "AppImage files" 72 | clean_item "build/*.zsync" "AppImage zsync files" 73 | 74 | # Find and remove any AppImage files in root or subdirectories 75 | find . -name "*.AppImage" -not -path "./releases/*" -exec rm -f {} \; 2>/dev/null 76 | if [ $? -eq 0 ]; then 77 | print_success "Removed stray AppImage files" 78 | fi 79 | 80 | # 2. Python cache and compiled files 81 | print_status "Cleaning Python cache files..." 82 | clean_item "src/__pycache__" "Python cache directory" 83 | clean_item "scripts/__pycache__" "Scripts cache directory" 84 | 85 | # Find and remove Python cache files recursively 86 | find . -name "*.pyc" -delete 2>/dev/null 87 | find . -name "*.pyo" -delete 2>/dev/null 88 | find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null 89 | if [ $? -eq 0 ]; then 90 | print_success "Removed Python cache files" 91 | fi 92 | 93 | # 3. Virtual environment (if present) 94 | if [ -d "venv" ]; then 95 | read -p "🤔 Remove virtual environment (venv/)? [y/N]: " -n 1 -r 96 | echo "" 97 | if [[ $REPLY =~ ^[Yy]$ ]]; then 98 | clean_item "venv" "Python virtual environment" 99 | else 100 | print_warning "Keeping virtual environment" 101 | fi 102 | fi 103 | 104 | # 4. Temporary files and logs 105 | print_status "Cleaning temporary files..." 106 | clean_item "*.log" "Log files" 107 | clean_item "*.tmp" "Temporary files" 108 | clean_item "*.temp" "Temporary files" 109 | clean_item ".cache" "Cache directory" 110 | clean_item "temp" "Temp directory" 111 | clean_item "tmp" "Tmp directory" 112 | 113 | # 5. Development artifacts 114 | print_status "Cleaning development artifacts..." 115 | clean_item ".pytest_cache" "Pytest cache" 116 | clean_item ".coverage" "Coverage files" 117 | clean_item "htmlcov" "Coverage HTML reports" 118 | clean_item ".mypy_cache" "MyPy cache" 119 | clean_item ".tox" "Tox cache" 120 | clean_item "dist" "Distribution directory" 121 | clean_item "*.egg-info" "Python egg info" 122 | 123 | # 6. Editor and IDE files 124 | print_status "Cleaning editor files..." 125 | clean_item ".vscode/settings.json" "VS Code workspace settings (keeping extensions)" 126 | clean_item "*.swp" "Vim swap files" 127 | clean_item "*.swo" "Vim swap files" 128 | clean_item "*~" "Editor backup files" 129 | 130 | # Find and remove editor backup files 131 | find . -name "*.swp" -delete 2>/dev/null 132 | find . -name "*.swo" -delete 2>/dev/null 133 | find . -name "*~" -delete 2>/dev/null 134 | 135 | # 7. AppImage tools (optional) 136 | if [ -f "/tmp/appimagetool" ]; then 137 | read -p "🤔 Remove downloaded appimagetool? [y/N]: " -n 1 -r 138 | echo "" 139 | if [[ $REPLY =~ ^[Yy]$ ]]; then 140 | clean_item "/tmp/appimagetool" "Downloaded appimagetool" 141 | else 142 | print_warning "Keeping appimagetool" 143 | fi 144 | fi 145 | 146 | # 8. Git artifacts (careful - only temporary ones) 147 | print_status "Cleaning git temporary files..." 148 | clean_item ".git/index.lock" "Git index lock" 149 | clean_item ".git/HEAD.lock" "Git HEAD lock" 150 | 151 | # 9. Test artifacts and screenshots 152 | print_status "Cleaning test artifacts..." 153 | clean_item "screenshot_*.png" "Test screenshots" 154 | clean_item "screen_*.png" "Test screenshots" 155 | clean_item "test_*.png" "Test images" 156 | 157 | # Find test screenshots 158 | find . -name "screenshot_*.png" -delete 2>/dev/null 159 | find . -name "screen_*.png" -delete 2>/dev/null 160 | 161 | # 10. User data (be very careful) 162 | if [ -d ".config" ]; then 163 | read -p "🤔 Remove user config directory (.config/)? [y/N]: " -n 1 -r 164 | echo "" 165 | if [[ $REPLY =~ ^[Yy]$ ]]; then 166 | clean_item ".config" "User configuration" 167 | else 168 | print_warning "Keeping user configuration" 169 | fi 170 | fi 171 | 172 | # 11. Clean empty directories 173 | print_status "Removing empty directories..." 174 | find . -type d -empty -not -path "./.git/*" -delete 2>/dev/null 175 | if [ $? -eq 0 ]; then 176 | print_success "Removed empty directories" 177 | fi 178 | 179 | # 12. Optional: Reset file permissions 180 | read -p "🔧 Reset file permissions? [y/N]: " -n 1 -r 181 | echo "" 182 | if [[ $REPLY =~ ^[Yy]$ ]]; then 183 | chmod +x scripts/*.sh 2>/dev/null 184 | chmod +x build/*.sh 2>/dev/null 185 | chmod +x dev_run.sh 2>/dev/null 186 | chmod 644 *.md 2>/dev/null 187 | chmod 644 *.txt 2>/dev/null 188 | print_success "Reset file permissions" 189 | fi 190 | 191 | # Calculate cleaned size in human readable format 192 | if [ $CLEANED_SIZE -gt 1073741824 ]; then 193 | SIZE_DISPLAY=$(echo "scale=1; $CLEANED_SIZE / 1073741824" | bc 2>/dev/null || echo "$(($CLEANED_SIZE / 1073741824))") 194 | SIZE_UNIT="GB" 195 | elif [ $CLEANED_SIZE -gt 1048576 ]; then 196 | SIZE_DISPLAY=$(echo "scale=1; $CLEANED_SIZE / 1048576" | bc 2>/dev/null || echo "$(($CLEANED_SIZE / 1048576))") 197 | SIZE_UNIT="MB" 198 | elif [ $CLEANED_SIZE -gt 1024 ]; then 199 | SIZE_DISPLAY=$(echo "scale=1; $CLEANED_SIZE / 1024" | bc 2>/dev/null || echo "$(($CLEANED_SIZE / 1024))") 200 | SIZE_UNIT="KB" 201 | else 202 | SIZE_DISPLAY=$CLEANED_SIZE 203 | SIZE_UNIT="bytes" 204 | fi 205 | 206 | # Summary 207 | echo "" 208 | echo "🎉 Clean Complete!" 209 | echo "=================" 210 | echo "" 211 | echo "📊 Summary:" 212 | echo " 🗑️ Items cleaned: $CLEANED_ITEMS" 213 | echo " 💾 Space freed: ${SIZE_DISPLAY} ${SIZE_UNIT}" 214 | echo "" 215 | echo "📋 What was cleaned:" 216 | echo " ✅ AppImage build artifacts" 217 | echo " ✅ Python cache files" 218 | echo " ✅ Temporary files and logs" 219 | echo " ✅ Development artifacts" 220 | echo " ✅ Editor backup files" 221 | echo " ✅ Empty directories" 222 | echo "" 223 | echo "📁 Repository status:" 224 | echo " ✅ Source code preserved" 225 | echo " ✅ Documentation preserved" 226 | echo " ✅ Git history preserved" 227 | echo " ✅ Configuration files preserved" 228 | echo "" 229 | 230 | # Verify project integrity 231 | if [ -f "src/romm_sync_app.py" ] && [ -f "README.md" ] && [ -f "LICENSE" ]; then 232 | print_success "Project integrity verified - all essential files present" 233 | else 234 | print_error "Project integrity check failed - some essential files missing!" 235 | fi 236 | 237 | echo "" 238 | echo "🚀 Ready for:" 239 | echo " • Fresh development" 240 | echo " • Clean build" 241 | echo " • Git operations" 242 | echo " • Release preparation" 243 | echo "" 244 | echo "💡 Tip: Run './build/build_appimage.sh' to create a fresh AppImage" -------------------------------------------------------------------------------- /scripts/setup_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # RomM-RetroArch Sync - Development Environment Setup 5 | # This script sets up the development environment for contributing to the project 6 | 7 | echo "🚀 RomM-RetroArch Sync - Development Environment Setup" 8 | echo "=====================================================" 9 | echo "" 10 | 11 | # Colors for output 12 | RED='\033[0;31m' 13 | GREEN='\033[0;32m' 14 | YELLOW='\033[1;33m' 15 | BLUE='\033[0;34m' 16 | NC='\033[0m' # No Color 17 | 18 | # Helper functions 19 | print_status() { 20 | echo -e "${BLUE}ℹ️ $1${NC}" 21 | } 22 | 23 | print_success() { 24 | echo -e "${GREEN}✅ $1${NC}" 25 | } 26 | 27 | print_warning() { 28 | echo -e "${YELLOW}⚠️ $1${NC}" 29 | } 30 | 31 | print_error() { 32 | echo -e "${RED}❌ $1${NC}" 33 | } 34 | 35 | # Check if script is run from project root 36 | if [ ! -f "src/romm_sync_app.py" ]; then 37 | print_error "Please run this script from the project root directory" 38 | exit 1 39 | fi 40 | 41 | print_status "Setting up development environment for RomM-RetroArch Sync..." 42 | echo "" 43 | 44 | # 1. Check system requirements 45 | print_status "Checking system requirements..." 46 | 47 | # Check OS 48 | if [[ "$OSTYPE" != "linux-gnu"* ]]; then 49 | print_error "This project is designed for Linux systems" 50 | exit 1 51 | fi 52 | 53 | # Check Python version 54 | if ! command -v python3 &> /dev/null; then 55 | print_error "Python 3 is required but not installed" 56 | exit 1 57 | fi 58 | 59 | PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))') 60 | REQUIRED_VERSION="3.8" 61 | 62 | if ! python3 -c "import sys; exit(0 if sys.version_info >= (3, 8) else 1)"; then 63 | print_error "Python 3.8+ required. Found: $PYTHON_VERSION" 64 | exit 1 65 | fi 66 | 67 | print_success "Python $PYTHON_VERSION found" 68 | 69 | # 2. Detect package manager and install system dependencies 70 | print_status "Installing system dependencies..." 71 | 72 | if command -v apt &> /dev/null; then 73 | # Debian/Ubuntu 74 | print_status "Detected apt package manager (Debian/Ubuntu)" 75 | 76 | sudo apt update 77 | sudo apt install -y \ 78 | python3-cryptography \ 79 | python3-dev \ 80 | python3-pip \ 81 | python3-venv \ 82 | python3-gi \ 83 | python3-gi-cairo \ 84 | gir1.2-gtk-4.0 \ 85 | gir1.2-adw-1 \ 86 | gir1.2-appindicator3-0.1 \ 87 | libgirepository1.0-dev \ 88 | gcc \ 89 | pkg-config \ 90 | wget \ 91 | git 92 | 93 | elif command -v dnf &> /dev/null; then 94 | # Fedora 95 | print_status "Detected dnf package manager (Fedora)" 96 | 97 | sudo dnf install -y \ 98 | python3-cryptography \ 99 | python3-devel \ 100 | python3-pip \ 101 | python3-gobject \ 102 | gtk4-devel \ 103 | libadwaita-devel \ 104 | libappindicator-gtk3-devel \ 105 | gobject-introspection-devel \ 106 | gcc \ 107 | pkg-config \ 108 | wget \ 109 | git 110 | 111 | elif command -v pacman &> /dev/null; then 112 | # Arch Linux 113 | print_status "Detected pacman package manager (Arch Linux)" 114 | 115 | sudo pacman -S --needed \ 116 | python \ 117 | python-cryptography \ 118 | python-pip \ 119 | python-gobject \ 120 | gtk4 \ 121 | libadwaita \ 122 | libappindicator-gtk3 \ 123 | gobject-introspection \ 124 | gcc \ 125 | pkgconf \ 126 | wget \ 127 | git 128 | 129 | else 130 | print_warning "Unknown package manager. Please install the following manually:" 131 | echo " - Python 3.8+ development headers" 132 | echo " - GTK4 and PyGObject" 133 | echo " - Libadwaita" 134 | echo " - AppIndicator3" 135 | echo " - GObject Introspection" 136 | echo " - Build tools (gcc, pkg-config)" 137 | fi 138 | 139 | print_success "System dependencies installed" 140 | 141 | # 3. Set up Python virtual environment (optional but recommended) 142 | print_status "Setting up Python virtual environment..." 143 | 144 | if [ ! -d "venv" ]; then 145 | python3 -m venv venv 146 | print_success "Virtual environment created" 147 | else 148 | print_warning "Virtual environment already exists" 149 | fi 150 | 151 | # Activate virtual environment 152 | source venv/bin/activate 153 | print_success "Virtual environment activated" 154 | 155 | # 4. Install Python dependencies 156 | print_status "Installing Python dependencies..." 157 | 158 | # Upgrade pip first 159 | pip install --upgrade pip 160 | 161 | # Install dependencies from requirements.txt 162 | if [ -f "requirements.txt" ]; then 163 | pip install -r requirements.txt 164 | print_success "Python dependencies installed from requirements.txt" 165 | else 166 | # Fallback to manual installation 167 | print_warning "requirements.txt not found, installing dependencies manually" 168 | pip install requests watchdog cryptography # <-- Add cryptography here 169 | fi 170 | 171 | # 5. Install development dependencies 172 | print_status "Installing development dependencies..." 173 | 174 | pip install \ 175 | pytest \ 176 | pytest-cov \ 177 | flake8 \ 178 | black \ 179 | mypy \ 180 | pre-commit 181 | 182 | print_success "Development dependencies installed" 183 | 184 | # 6. Set up pre-commit hooks (optional) 185 | print_status "Setting up pre-commit hooks..." 186 | 187 | if command -v pre-commit &> /dev/null; then 188 | # Create .pre-commit-config.yaml if it doesn't exist 189 | if [ ! -f ".pre-commit-config.yaml" ]; then 190 | cat > .pre-commit-config.yaml << 'EOF' 191 | repos: 192 | - repo: https://github.com/psf/black 193 | rev: 23.3.0 194 | hooks: 195 | - id: black 196 | language_version: python3 197 | 198 | - repo: https://github.com/pycqa/flake8 199 | rev: 6.0.0 200 | hooks: 201 | - id: flake8 202 | args: [--max-line-length=88, --extend-ignore=E203] 203 | 204 | - repo: https://github.com/pre-commit/pre-commit-hooks 205 | rev: v4.4.0 206 | hooks: 207 | - id: trailing-whitespace 208 | - id: end-of-file-fixer 209 | - id: check-yaml 210 | - id: check-added-large-files 211 | EOF 212 | print_success "Created .pre-commit-config.yaml" 213 | fi 214 | 215 | pre-commit install 216 | print_success "Pre-commit hooks installed" 217 | else 218 | print_warning "pre-commit not available, skipping hooks setup" 219 | fi 220 | 221 | # 7. Verify installation 222 | print_status "Verifying installation..." 223 | 224 | # Test Python imports 225 | python3 -c " 226 | import gi 227 | gi.require_version('Gtk', '4.0') 228 | gi.require_version('Adw', '1') 229 | from gi.repository import Gtk, Adw 230 | import requests 231 | import watchdog 232 | print('✅ All imports successful') 233 | " && print_success "Python dependencies verified" || print_error "Python import verification failed" 234 | 235 | # Test syntax of main application 236 | python3 -m py_compile src/romm_sync_app.py && print_success "Main application syntax verified" || print_error "Main application syntax check failed" 237 | 238 | # 8. Install appimagetool for building 239 | print_status "Setting up AppImage build tools..." 240 | 241 | if [ ! -f "/usr/local/bin/appimagetool" ] && [ ! -f "/tmp/appimagetool" ]; then 242 | print_status "Downloading appimagetool..." 243 | wget -O /tmp/appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage 244 | chmod +x /tmp/appimagetool 245 | 246 | # Ask if user wants to install system-wide 247 | echo "" 248 | read -p "Install appimagetool system-wide? [y/N]: " -n 1 -r 249 | echo "" 250 | if [[ $REPLY =~ ^[Yy]$ ]]; then 251 | sudo mv /tmp/appimagetool /usr/local/bin/appimagetool 252 | print_success "appimagetool installed system-wide" 253 | else 254 | print_success "appimagetool downloaded to /tmp/appimagetool" 255 | fi 256 | else 257 | print_success "appimagetool already available" 258 | fi 259 | 260 | # 9. Test build process 261 | print_status "Testing build process..." 262 | 263 | if [ -f "build/build_appimage.sh" ]; then 264 | cd build 265 | ./build_appimage.sh > /dev/null 2>&1 && print_success "AppImage build test passed" || print_warning "AppImage build test failed (check dependencies)" 266 | cd .. 267 | else 268 | print_warning "Build script not found" 269 | fi 270 | 271 | # 10. Final setup 272 | print_status "Final setup..." 273 | 274 | # Create development helper script 275 | cat > dev_run.sh << 'EOF' 276 | #!/bin/bash 277 | # Development runner script 278 | 279 | # Activate virtual environment if it exists 280 | if [ -d "venv" ]; then 281 | source venv/bin/activate 282 | echo "🐍 Virtual environment activated" 283 | fi 284 | 285 | # Run the application 286 | echo "🚀 Starting RomM-RetroArch Sync..." 287 | python3 src/romm_sync_app.py 288 | EOF 289 | 290 | chmod +x dev_run.sh 291 | print_success "Created dev_run.sh helper script" 292 | 293 | # Summary 294 | echo "" 295 | echo "🎉 Development Environment Setup Complete!" 296 | echo "========================================" 297 | echo "" 298 | echo "📋 What was installed:" 299 | echo " ✅ System dependencies (GTK4, PyGObject, etc.)" 300 | echo " ✅ Python virtual environment (venv/)" 301 | echo " ✅ Project dependencies (requests, watchdog)" 302 | echo " ✅ Development tools (pytest, flake8, black)" 303 | echo " ✅ Pre-commit hooks (code quality)" 304 | echo " ✅ AppImage build tools" 305 | echo "" 306 | echo "🚀 Quick Start:" 307 | echo " # Run the application:" 308 | echo " ./dev_run.sh" 309 | echo "" 310 | echo " # Or manually:" 311 | echo " source venv/bin/activate" 312 | echo " python3 src/romm_sync_app.py" 313 | echo "" 314 | echo " # Build AppImage:" 315 | echo " cd build && ./build_appimage.sh" 316 | echo "" 317 | echo " # Run tests:" 318 | echo " source venv/bin/activate" 319 | echo " pytest" 320 | echo "" 321 | echo "📚 Development Guide:" 322 | echo " - Code style: Use 'black' for formatting" 323 | echo " - Linting: Use 'flake8' for style checking" 324 | echo " - Testing: Add tests in tests/ directory" 325 | echo " - Git hooks: Pre-commit hooks ensure code quality" 326 | echo "" 327 | echo "🤝 Ready to contribute! Happy coding! 🎯" -------------------------------------------------------------------------------- /romm_platform_slugs.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "source": "https://docs.romm.app/4.3.0/Platforms-and-Players/Supported-Platforms/", 4 | "total_platforms": 455, 5 | "date_extracted": "2025-11-09" 6 | }, 7 | "platforms": { 8 | "1292 Advanced Programmable Video System": "1292-advanced-programmable-video-system", 9 | "3DO Interactive Multiplayer": "3do", 10 | "8-Bit Productions Commander X16": "commander-x16", 11 | "Aamber Pegasus": "pegasus", 12 | "ABC 80": "abc-80", 13 | "Acorn Archimedes": "acorn-archimedes", 14 | "Acorn Electron": "acorn-electron", 15 | "Advanced Pico Beena": "advanced-pico-beena", 16 | "Adventure Vision": "adventure-vision", 17 | "AirConsole": "airconsole", 18 | "Alice 32/90": "alice-3290", 19 | "Altair 680": "altair-680", 20 | "Altair 8800": "altair-8800", 21 | "Amazon Alexa": "amazon-alexa", 22 | "Amazon Fire TV": "amazon-fire-tv", 23 | "Amiga": "amiga", 24 | "Amiga CD": "amiga-cd", 25 | "Amiga CD32": "amiga-cd32", 26 | "Amstrad CPC": "acpc", 27 | "Amstrad GX4000": "amstrad-gx4000", 28 | "Amstrad PCW": "amstrad-pcw", 29 | "Analogue electronics": "analogueelectronics", 30 | "Android": "android", 31 | "Antstream": "antstream", 32 | "APF MP1000/Imagination Machine": "apf", 33 | "Apogee BK-01": "bk-01", 34 | "Apple I": "apple", 35 | "Apple II": "appleii", 36 | "Apple IIGS": "apple-iigs", 37 | "Apple III": "appleiii", 38 | "Apple Lisa": "apple-lisa", 39 | "Apple Pippin": "apple-pippin", 40 | "Arcade": "arcade", 41 | "Arcadia 2001": "arcadia-2001", 42 | "Arduboy": "arduboy", 43 | "Astral 2000": "astral-2000", 44 | "Atari 2600": "atari2600", 45 | "Atari 5200": "atari5200", 46 | "Atari 7800": "atari7800", 47 | "Atari 8-bit": "atari8bit", 48 | "Atari 800": "atari800", 49 | "Atari Jaguar": "jaguar", 50 | "Atari Jaguar CD": "atari-jaguar-cd", 51 | "Atari Lynx": "lynx", 52 | "Atari ST/STE": "atari-st", 53 | "Atari VCS": "atari-vcs", 54 | "Atari XEGS": "atari-xegs", 55 | "Atom": "atom", 56 | "AY-3-8500": "ay-3-8500", 57 | "AY-3-8603": "ay-3-8603", 58 | "AY-3-8605": "ay-3-8605", 59 | "AY-3-8606": "ay-3-8606", 60 | "AY-3-8607": "ay-3-8607", 61 | "AY-3-8610": "ay-3-8610", 62 | "AY-3-8710": "ay-3-8710", 63 | "AY-3-8760": "ay-3-8760", 64 | "Bada": "bada", 65 | "Bally Astrocade": "astrocade", 66 | "BBC Microcomputer System": "bbcmicro", 67 | "Benesse Pocket Challenge V2": "pocket-challenge-v2", 68 | "Benesse Pocket Challenge W": "pocket-challenge-w", 69 | "BeOS": "beos", 70 | "BGR Computers Excalibur 64": "excalibur-64", 71 | "Bit Corporation BIT 90": "bit-90", 72 | "Black Point": "black-point", 73 | "BlackBerry OS": "blackberry", 74 | "Blacknut": "blacknut", 75 | "Blu-ray Player": "blu-ray-player", 76 | "BREW": "brew", 77 | "Browser (Flash/HTML5)": "browser", 78 | "Bubble": "bubble", 79 | "Call-A-Computer time-shared mainframe computer system": "call-a-computer", 80 | "Cambridge Computer Z88": "z88", 81 | "Camputers Lynx": "camputers-lynx", 82 | "Casio CFX-9850": "casio-cfx-9850", 83 | "Casio FP-1000 & FP-1100": "casio-fp-1000", 84 | "Casio Loopy": "casio-loopy", 85 | "Casio PB-1000": "casio-pb-1000", 86 | "Casio Programmable Calculator": "casio-programmable-calculator", 87 | "Casio PV-1000": "casio-pv-1000", 88 | "Casio PV-2000": "casio-pv-2000", 89 | "CDC Cyber 70": "cdccyber70", 90 | "Champion 2711": "champion-2711", 91 | "ClickStart": "clickstart", 92 | "Coleco Adam": "colecoadam", 93 | "ColecoVision": "colecovision", 94 | "Colour Genie": "colour-genie", 95 | "Commodore 128": "c128", 96 | "Commodore 16": "c16", 97 | "Commodore C64/128/MAX": "c64", 98 | "Commodore CDTV": "commodore-cdtv", 99 | "Commodore PET": "cpet", 100 | "Commodore Plus/4": "c-plus-4", 101 | "Commodore VIC-20": "vic-20", 102 | "Compal 80": "compal-80", 103 | "Compucolor I": "compucolor-i", 104 | "Compucolor II": "compucolor-ii", 105 | "Compucorp Programmable Calculator": "compucorp-programmable-calculator", 106 | "COSMAC": "fred-cosmac", 107 | "CP/M": "cpm", 108 | "CreatiVision": "creativision", 109 | "Cybervision": "cybervision", 110 | "Danger OS": "danger-os", 111 | "Daydream": "daydream", 112 | "DEC GT40": "gt40", 113 | "Dedicated console": "dedicated-console", 114 | "Dedicated handheld": "dedicated-handheld", 115 | "Didj": "didj", 116 | "Digiblast": "digiblast", 117 | "DoJa": "doja", 118 | "Donner Model 30": "donner30", 119 | "DOS": "dos", 120 | "Dragon 32/64": "dragon-32-slash-64", 121 | "Dreamcast": "dc", 122 | "DVD Player": "dvd-player", 123 | "e-Reader / Card-e Reader": "e-reader-slash-card-e-reader", 124 | "ECD Micromind": "ecd-micromind", 125 | "EDSAC": "edsac", 126 | "Elektor TV Games Computer": "elektor", 127 | "Elektronika BK": "bk", 128 | "Enterprise": "enterprise", 129 | "Epoch Cassette Vision": "epoch-cassette-vision", 130 | "Epoch Game Pocket Computer": "epoch-game-pocket-computer", 131 | "Epoch Super Cassette Vision": "epoch-super-cassette-vision", 132 | "Evercade": "evercade", 133 | "Exelvision": "exelvision", 134 | "ExEn": "exen", 135 | "Exidy Sorcerer": "exidy-sorcerer", 136 | "Fairchild Channel F": "fairchild-channel-f", 137 | "Family Computer": "famicom", 138 | "Family Computer Disk System": "fds", 139 | "Feature phone": "mobile-custom", 140 | "Ferranti Nimrod Computer": "nimrod", 141 | "FM Towns": "fm-towns", 142 | "FM-7": "fm-7", 143 | "Freebox": "freebox", 144 | "G-cluster": "g-cluster", 145 | "Galaksija": "galaksija", 146 | "Gamate": "gamate", 147 | "Game & Watch": "g-and-w", 148 | "Game Boy": "gb", 149 | "Game Boy Advance": "gba", 150 | "Game Boy Color": "gbc", 151 | "Game Master": "hartung", 152 | "Game Wave": "game-wave", 153 | "Game.com": "game-dot-com", 154 | "GameStick": "gamestick", 155 | "Gear VR": "gear-vr", 156 | "GIMINI": "gimini", 157 | "Gizmondo": "gizmondo", 158 | "Gloud": "gloud", 159 | "Glulx": "glulx", 160 | "GNEX": "gnex", 161 | "Google Stadia": "stadia", 162 | "GP2X": "gp2x", 163 | "GP2X Wiz": "gp2x-wiz", 164 | "GP32": "gp32", 165 | "GVM": "gvm", 166 | "Handheld Electronic LCD": "handheld-electronic-lcd", 167 | "HD DVD Player": "hd-dvd-player", 168 | "Heath/Zenith H8/H89": "heathzenith", 169 | "Heathkit H11": "heathkit-h11", 170 | "Hector HRX": "hrx", 171 | "Hitachi S1": "hitachi-s1", 172 | "HP 2100": "hp2100", 173 | "HP 3000": "hp3000", 174 | "HP 9800": "hp-9800", 175 | "HP Programmable Calculator": "hp-programmable-calculator", 176 | "Hugo": "hugo", 177 | "Hyper Neo Geo 64": "hyper-neo-geo-64", 178 | "HyperScan": "hyperscan", 179 | "IBM 5100": "ibm-5100", 180 | "IBM PCjr": "pc-jr", 181 | "Ideal-Computer": "ideal-computer", 182 | "iiRcade": "iircade", 183 | "Imlac PDS-1": "imlac-pds1", 184 | "Intel 8008": "intel-8008", 185 | "Intel 8080": "intel-8080", 186 | "Intel 8086 / 8088": "intel-8086", 187 | "Intellivision": "intellivision", 188 | "Intellivision Amico": "intellivision-amico", 189 | "Interact Model One": "interact-model-one", 190 | "Interton VC 4000": "interton-vc-4000", 191 | "Interton Video 2000": "interton-video-2000", 192 | "iOS": "ios", 193 | "iPad": "ipad", 194 | "iPod Classic": "ipod-classic", 195 | "J2ME": "j2me", 196 | "Jolt": "jolt", 197 | "Jupiter Ace": "jupiter-ace", 198 | "KaiOS": "kaios", 199 | "KIM-1": "kim-1", 200 | "Kindle Classic": "kindle", 201 | "Laser 200": "laser200", 202 | "LaserActive": "laseractive", 203 | "LeapFrog Explorer": "leapfrog-explorer", 204 | "Leapster": "leapster", 205 | "Leapster Explorer/LeadPad Explorer": "leapster-explorer-slash-leadpad-explorer", 206 | "LeapTV": "leaptv", 207 | "Legacy Computer": "legacy-computer", 208 | "Legacy Mobile Device": "mobile", 209 | "Linux": "linux", 210 | "Luna": "luna", 211 | "Mac": "mac", 212 | "Maemo": "maemo", 213 | "Magnavox Odyssey": "odyssey", 214 | "Mainframe": "mainframe", 215 | "Matsushita/Panasonic JR": "matsushitapanasonic-jr", 216 | "Mattel Aquarius": "aquarius", 217 | "MeeGo": "meego", 218 | "Mega Duck/Cougar Boy": "mega-duck-slash-cougar-boy", 219 | "Memotech MTX": "memotech-mtx", 220 | "Memotech MTX512": "mtx512", 221 | "Meritum": "meritum", 222 | "Meta Quest 2": "meta-quest-2", 223 | "Meta Quest 3": "meta-quest-3", 224 | "Microbee": "microbee", 225 | "Microcomputer": "microcomputer", 226 | "Microsoft MSX2+": "msx2plus", 227 | "Microtan 65": "microtan-65", 228 | "Microvision": "microvision", 229 | "Mophun": "mophun", 230 | "MOS Technology 6502": "mos-technology-6502", 231 | "Motorola 6800": "motorola-6800", 232 | "Motorola 68k": "motorola-68k", 233 | "MRE": "mre", 234 | "MSX": "msx", 235 | "MSX Turbo R": "msx-turbo", 236 | "MSX2": "msx2", 237 | "MUGEN": "mugen", 238 | "N-Gage": "ngage", 239 | "N-Gage (service)": "ngage2", 240 | "Namco System 22": "system-32", 241 | "Nascom": "nascom", 242 | "NEC PC-6000 Series": "nec-pc-6000-series", 243 | "Neo Geo AES": "neogeoaes", 244 | "Neo Geo CD": "neo-geo-cd", 245 | "Neo Geo MVS": "neogeomvs", 246 | "Neo Geo Pocket": "neo-geo-pocket", 247 | "Neo Geo Pocket Color": "neo-geo-pocket-color", 248 | "Neo Geo X": "neo-geo-x", 249 | "New Nintendo 3DS": "new-nintendo-3ds", 250 | "NewBrain": "newbrain", 251 | "Newton": "newton", 252 | "Nintendo 3DS": "3ds", 253 | "Nintendo 64": "n64", 254 | "Nintendo 64DD": "64dd", 255 | "Nintendo DS": "nds", 256 | "Nintendo DSi": "nintendo-dsi", 257 | "Nintendo Entertainment System": "nes", 258 | "Nintendo GameCube": "ngc", 259 | "Nintendo Switch": "switch", 260 | "Nintendo Switch 2": "switch-2", 261 | "North Star": "northstar", 262 | "Noval 760": "noval-760", 263 | "Nuon": "nuon", 264 | "Oculus Go": "oculus-go", 265 | "Oculus Quest": "oculus-quest", 266 | "Oculus Rift": "oculus-rift", 267 | "Oculus VR": "oculus-vr", 268 | "Odyssey 2": "odyssey-2", 269 | "Odyssey 2 / Videopac G7000": "odyssey-2-slash-videopac-g7000", 270 | "Ohio Scientific": "ohio-scientific", 271 | "OnLive Game System": "onlive-game-system", 272 | "OOParts": "ooparts", 273 | "OpenBOR": "openbor", 274 | "Orao": "orao", 275 | "Oric": "oric", 276 | "Oric Atmos": "atmos", 277 | "OS/2": "os2", 278 | "Othello Multivision": "multivision", 279 | "Ouya": "ouya", 280 | "Palm OS": "palm-os", 281 | "Palmtex": "palmtex", 282 | "Panasonic Jungle": "panasonic-jungle", 283 | "Panasonic M2": "panasonic-m2", 284 | "Pandora": "pandora", 285 | "PC Booter": "pc-booter", 286 | "PC Engine SuperGrafx": "supergrafx", 287 | "PC-50X Family": "pc-50x-family", 288 | "PC-6001": "pc-6001", 289 | "PC-8000": "pc-8000", 290 | "PC-8800 Series": "pc-8800-series", 291 | "PC-9800 Series": "pc-9800-series", 292 | "PC-FX": "pc-fx", 293 | "PDP-1": "pdp1", 294 | "PDP-10": "pdp10", 295 | "PDP-11": "pdp11", 296 | "PDP-7": "pdp-7", 297 | "PDP-8": "pdp-8", 298 | "Pebble": "pebble", 299 | "Philips CD-i": "philips-cd-i", 300 | "Philips VG 5000": "philips-vg-5000", 301 | "Photo CD": "photocd", 302 | "PICO": "pico", 303 | "Pinball": "pinball", 304 | "Pippin": "pippin", 305 | "PLATO": "plato", 306 | "Playdate": "playdate", 307 | "Playdia": "playdia", 308 | "PlayStation": "psx", 309 | "PlayStation 2": "ps2", 310 | "PlayStation 3": "ps3", 311 | "PlayStation 4": "ps4", 312 | "PlayStation 5": "ps5", 313 | "PlayStation Now": "playstation-now", 314 | "PlayStation Portable": "psp", 315 | "PlayStation Vita": "psvita", 316 | "PlayStation VR": "psvr", 317 | "PlayStation VR2": "psvr2", 318 | "Plex Arcade": "plex-arcade", 319 | "Plug & Play": "plug-and-play", 320 | "PocketStation": "pocketstation", 321 | "Pokitto": "pokitto", 322 | "Pokémon mini": "pokemon-mini", 323 | "Poly-88": "poly-88", 324 | "Polymega": "polymega", 325 | "R-Zone": "r-zone", 326 | "RCA Studio II": "rca-studio-ii", 327 | "Research Machines 380Z": "research-machines-380z", 328 | "Roku": "roku", 329 | "SAM Coupé": "sam-coupe", 330 | "Satellaview": "satellaview", 331 | "SC/MP": "scmp", 332 | "ScummVM": "scummvm", 333 | "SD-200/270/290": "sd-200270290", 334 | "SDS Sigma 7": "sdssigma7", 335 | "Sega 32X": "sega32", 336 | "Sega Advanced Pico Beena": "beena", 337 | "Sega CD": "segacd", 338 | "Sega CD 32X": "segacd32", 339 | "Sega Dreamcast VMU": "vmu", 340 | "Sega Game Gear": "gamegear", 341 | "Sega Hikaru": "hikaru", 342 | "Sega Master System/Mark III": "sms", 343 | "Sega Mega Drive/Genesis": "genesis", 344 | "Sega Model 1": "model1", 345 | "Sega Model 2": "model2", 346 | "Sega Model 3": "model3", 347 | "Sega Pico": "sega-pico", 348 | "Sega Saturn": "saturn", 349 | "Sega SC-3000": "sc3000", 350 | "Sega ST-V": "stv", 351 | "Sega System 16": "system16", 352 | "Sega System 32": "system32", 353 | "SG-1000": "sg1000", 354 | "Sharp MZ-2200": "sharp-mz-2200", 355 | "Sharp MZ-80B/2000/2500": "sharp-mz-80b20002500", 356 | "Sharp MZ-80K/700/800/1500": "sharp-mz-80k7008001500", 357 | "Sharp X1": "x1", 358 | "Sharp X68000": "sharp-x68000", 359 | "Sharp Zaurus": "sharp-zaurus", 360 | "Signetics 2650": "signetics-2650", 361 | "Sinclair QL": "sinclair-ql", 362 | "Sinclair ZX81": "zx81", 363 | "SK-VM": "sk-vm", 364 | "SMC-777": "smc-777", 365 | "Socrates": "socrates", 366 | "Sol-20": "sol-20", 367 | "Sony PSP Minis": "psp-minis", 368 | "Sord M5": "sord-m5", 369 | "Spectravideo": "spectravideo", 370 | "SRI-500/1000": "sri-5001000", 371 | "SteamVR": "steam-vr", 372 | "Sufami Turbo": "sufami-turbo", 373 | "Super A'Can": "super-acan", 374 | "Super Famicom": "sfam", 375 | "Super NES CD-ROM System": "super-nes-cd-rom-system", 376 | "Super Nintendo Entertainment System": "snes", 377 | "Super Vision 8000": "super-vision-8000", 378 | "Sure Shot HD": "sure-shot-hd", 379 | "SwanCrystal": "swancrystal", 380 | "SWTPC 6800": "swtpc-6800", 381 | "Symbian": "symbian", 382 | "TADS": "tads", 383 | "Taito Type X": "type-x", 384 | "Taito X-55": "taito-x-55", 385 | "Tandy Vis": "tandy-vis", 386 | "Tapwave Zodiac": "zod", 387 | "Tatung Einstein": "tatung-einstein", 388 | "Tektronix 4050": "tektronix-4050", 389 | "Tele-Spiel ES-2201": "tele-spiel", 390 | "Telstar Arcade": "telstar-arcade", 391 | "Terebikko / See 'n Say Video Phone": "terebikko-slash-see-n-say-video-phone", 392 | "Terminal": "terminal", 393 | "Texas Instruments TI-82": "ti-82", 394 | "Texas Instruments TI-83": "ti-83", 395 | "Texas Instruments TI-99": "ti-99", 396 | "Thomson MO5": "thomson-mo5", 397 | "Thomson TO": "thomson-to", 398 | "TI Programmable Calculator": "ti-programmable-calculator", 399 | "TI-99/4A": "ti-994a", 400 | "Tiki 100": "tiki-100", 401 | "TIM": "tim", 402 | "Timex Sinclair 2068": "timex-sinclair-2068", 403 | "Tizen": "tizen", 404 | "Tomahawk F1": "tomahawk-f1", 405 | "Tomy Tutor": "tomy-tutor", 406 | "Tomy Tutor / Pyuta / Grandstand Tutor": "tomy-tutor-slash-pyuta-slash-grandstand-tutor", 407 | "Triton": "triton", 408 | "TRS-80": "trs-80", 409 | "TRS-80 Color Computer": "trs-80-color-computer", 410 | "TRS-80 MC-10": "trs-80-mc-10", 411 | "TRS-80 Model 100": "trs-80-model-100", 412 | "TurboGrafx-16/PC Engine": "tg16", 413 | "Turbografx-16/PC Engine CD": "turbografx-cd", 414 | "tvOS": "tvos", 415 | "Uzebox": "uzebox", 416 | "V.Flash": "vflash", 417 | "V.Smile": "vsmile", 418 | "VC 4000": "vc-4000", 419 | "Vector-06C": "06c", 420 | "Vectrex": "vectrex", 421 | "Versatile": "versatile", 422 | "VideoBrain": "videobrain", 423 | "Videopac+ G7400": "videopac-g7400", 424 | "Virtual Boy": "virtualboy", 425 | "Virtual Console": "vc", 426 | "VIS": "vis", 427 | "visionOS": "visionos", 428 | "Visual Memory Unit / Visual Memory System": "visual-memory-unit-slash-visual-memory-system", 429 | "Wang 2200": "wang2200", 430 | "WASM-4": "wasm-4", 431 | "Watara/QuickShot Supervision": "supervision", 432 | "watchOS": "watchos", 433 | "webOS": "webos", 434 | "Wii": "wii", 435 | "Wii U": "wiiu", 436 | "Windows": "win", 437 | "Windows 3.x": "win3x", 438 | "Windows Apps": "windows-apps", 439 | "Windows Mixed Reality": "windows-mixed-reality", 440 | "Windows Mobile": "windows-mobile", 441 | "Windows Phone": "winphone", 442 | "WIPI": "wipi", 443 | "WonderSwan": "wonderswan", 444 | "WonderSwan Color": "wonderswan-color", 445 | "WoW Action Max": "action-max", 446 | "XaviXPORT": "xavixport", 447 | "Xbox": "xbox", 448 | "Xbox 360": "xbox360", 449 | "Xbox Cloud Gaming": "xboxcloudgaming", 450 | "Xbox One": "xboxone", 451 | "Xbox Series X": "series-x-s", 452 | "Xerox Alto": "xerox-alto", 453 | "Z-machine": "z-machine", 454 | "Zeebo": "zeebo", 455 | "Zilog Z80": "z80", 456 | "Zilog Z8000": "zilog-z8000", 457 | "ZiNc": "zinc", 458 | "Zodiac": "zodiac", 459 | "Zune": "zune", 460 | "ZX Spectrum": "zxs", 461 | "ZX Spectrum Next": "zx-spectrum-next", 462 | "ZX80": "zx80" 463 | }, 464 | "aliases": { 465 | "NES": "nes", 466 | "SNES": "snes", 467 | "Famicom": "famicom", 468 | "Super Famicom": "sfam", 469 | "Genesis": "genesis", 470 | "Mega Drive": "genesis", 471 | "PlayStation": "psx", 472 | "PS1": "psx", 473 | "PS2": "ps2", 474 | "PS3": "ps3", 475 | "PS4": "ps4", 476 | "PS5": "ps5", 477 | "PSP": "psp", 478 | "PS Vita": "psvita", 479 | "Vita": "psvita", 480 | "Game Boy": "gb", 481 | "GBA": "gba", 482 | "GBC": "gbc", 483 | "N64": "n64", 484 | "GameCube": "ngc", 485 | "Wii": "wii", 486 | "Wii U": "wiiu", 487 | "Switch": "switch", 488 | "3DS": "3ds", 489 | "DS": "nds", 490 | "Dreamcast": "dc", 491 | "Saturn": "saturn", 492 | "Master System": "sms", 493 | "Game Gear": "gamegear", 494 | "Atari 2600": "atari2600", 495 | "Atari 5200": "atari5200", 496 | "Atari 7800": "atari7800", 497 | "Jaguar": "jaguar", 498 | "Lynx": "lynx", 499 | "Xbox": "xbox", 500 | "Xbox 360": "xbox360", 501 | "Xbox One": "xboxone", 502 | "PC": "win", 503 | "Windows": "win", 504 | "Mac": "mac", 505 | "macOS": "mac", 506 | "Linux": "linux", 507 | "DOS": "dos", 508 | "Arcade": "arcade", 509 | "MAME": "arcade", 510 | "Neo Geo": "neogeoaes", 511 | "Neo Geo AES": "neogeoaes", 512 | "Neo Geo MVS": "neogeomvs", 513 | "Neo Geo Pocket": "neo-geo-pocket", 514 | "NGP": "neo-geo-pocket", 515 | "NGPC": "neo-geo-pocket-color", 516 | "TurboGrafx-16": "tg16", 517 | "TG16": "tg16", 518 | "PC Engine": "tg16", 519 | "PCE": "tg16", 520 | "3DO": "3do", 521 | "Amiga": "amiga", 522 | "C64": "c64", 523 | "Commodore 64": "c64", 524 | "ZX Spectrum": "zxs", 525 | "Spectrum": "zxs" 526 | } 527 | } -------------------------------------------------------------------------------- /romm_platform_slugs.py: -------------------------------------------------------------------------------- 1 | """ 2 | RomM Platform Slug Mapping 3 | Extracted from: https://docs.romm.app/4.3.0/Platforms-and-Players/Supported-Platforms/ 4 | Total platforms: 455 5 | Date extracted: 2025-11-09 6 | """ 7 | 8 | ROMM_PLATFORM_SLUGS = { 9 | "1292 Advanced Programmable Video System": "1292-advanced-programmable-video-system", 10 | "3DO Interactive Multiplayer": "3do", 11 | "8-Bit Productions Commander X16": "commander-x16", 12 | "ABC 80": "abc-80", 13 | "APF MP1000/Imagination Machine": "apf", 14 | "AY-3-8500": "ay-3-8500", 15 | "AY-3-8603": "ay-3-8603", 16 | "AY-3-8605": "ay-3-8605", 17 | "AY-3-8606": "ay-3-8606", 18 | "AY-3-8607": "ay-3-8607", 19 | "AY-3-8610": "ay-3-8610", 20 | "AY-3-8710": "ay-3-8710", 21 | "AY-3-8760": "ay-3-8760", 22 | "Aamber Pegasus": "pegasus", 23 | "Acorn Archimedes": "acorn-archimedes", 24 | "Acorn Electron": "acorn-electron", 25 | "Advanced Pico Beena": "advanced-pico-beena", 26 | "Adventure Vision": "adventure-vision", 27 | "AirConsole": "airconsole", 28 | "Alice 32/90": "alice-3290", 29 | "Altair 680": "altair-680", 30 | "Altair 8800": "altair-8800", 31 | "Amazon Alexa": "amazon-alexa", 32 | "Amazon Fire TV": "amazon-fire-tv", 33 | "Amiga": "amiga", 34 | "Amiga CD": "amiga-cd", 35 | "Amiga CD32": "amiga-cd32", 36 | "Amstrad CPC": "acpc", 37 | "Amstrad GX4000": "amstrad-gx4000", 38 | "Amstrad PCW": "amstrad-pcw", 39 | "Analogue electronics": "analogueelectronics", 40 | "Android": "android", 41 | "Antstream": "antstream", 42 | "Apogee BK-01": "bk-01", 43 | "Apple I": "apple", 44 | "Apple II": "appleii", 45 | "Apple IIGS": "apple-iigs", 46 | "Apple III": "appleiii", 47 | "Apple Lisa": "apple-lisa", 48 | "Apple Pippin": "apple-pippin", 49 | "Arcade": "arcade", 50 | "Arcadia 2001": "arcadia-2001", 51 | "Arduboy": "arduboy", 52 | "Astral 2000": "astral-2000", 53 | "Atari 2600": "atari2600", 54 | "Atari 5200": "atari5200", 55 | "Atari 7800": "atari7800", 56 | "Atari 8-bit": "atari8bit", 57 | "Atari 800": "atari800", 58 | "Atari Jaguar": "jaguar", 59 | "Atari Jaguar CD": "atari-jaguar-cd", 60 | "Atari Lynx": "lynx", 61 | "Atari ST/STE": "atari-st", 62 | "Atari VCS": "atari-vcs", 63 | "Atari XEGS": "atari-xegs", 64 | "Atom": "atom", 65 | "BBC Microcomputer System": "bbcmicro", 66 | "BGR Computers Excalibur 64": "excalibur-64", 67 | "BREW": "brew", 68 | "Bada": "bada", 69 | "Bally Astrocade": "astrocade", 70 | "BeOS": "beos", 71 | "Benesse Pocket Challenge V2": "pocket-challenge-v2", 72 | "Benesse Pocket Challenge W": "pocket-challenge-w", 73 | "Bit Corporation BIT 90": "bit-90", 74 | "Black Point": "black-point", 75 | "BlackBerry OS": "blackberry", 76 | "Blacknut": "blacknut", 77 | "Blu-ray Player": "blu-ray-player", 78 | "Browser (Flash/HTML5)": "browser", 79 | "Bubble": "bubble", 80 | "CDC Cyber 70": "cdccyber70", 81 | "COSMAC": "fred-cosmac", 82 | "CP/M": "cpm", 83 | "Call-A-Computer time-shared mainframe computer system": "call-a-computer", 84 | "Cambridge Computer Z88": "z88", 85 | "Camputers Lynx": "camputers-lynx", 86 | "Casio CFX-9850": "casio-cfx-9850", 87 | "Casio FP-1000 & FP-1100": "casio-fp-1000", 88 | "Casio Loopy": "casio-loopy", 89 | "Casio PB-1000": "casio-pb-1000", 90 | "Casio PV-1000": "casio-pv-1000", 91 | "Casio PV-2000": "casio-pv-2000", 92 | "Casio Programmable Calculator": "casio-programmable-calculator", 93 | "Champion 2711": "champion-2711", 94 | "ClickStart": "clickstart", 95 | "Coleco Adam": "colecoadam", 96 | "ColecoVision": "colecovision", 97 | "Colour Genie": "colour-genie", 98 | "Commodore 128": "c128", 99 | "Commodore 16": "c16", 100 | "Commodore C64/128/MAX": "c64", 101 | "Commodore CDTV": "commodore-cdtv", 102 | "Commodore PET": "cpet", 103 | "Commodore Plus/4": "c-plus-4", 104 | "Commodore VIC-20": "vic-20", 105 | "Compal 80": "compal-80", 106 | "Compucolor I": "compucolor-i", 107 | "Compucolor II": "compucolor-ii", 108 | "Compucorp Programmable Calculator": "compucorp-programmable-calculator", 109 | "CreatiVision": "creativision", 110 | "Cybervision": "cybervision", 111 | "DEC GT40": "gt40", 112 | "DOS": "dos", 113 | "DVD Player": "dvd-player", 114 | "Danger OS": "danger-os", 115 | "Daydream": "daydream", 116 | "Dedicated console": "dedicated-console", 117 | "Dedicated handheld": "dedicated-handheld", 118 | "Didj": "didj", 119 | "Digiblast": "digiblast", 120 | "DoJa": "doja", 121 | "Donner Model 30": "donner30", 122 | "Dragon 32/64": "dragon-32-slash-64", 123 | "Dreamcast": "dc", 124 | "ECD Micromind": "ecd-micromind", 125 | "EDSAC": "edsac", 126 | "Elektor TV Games Computer": "elektor", 127 | "Elektronika BK": "bk", 128 | "Enterprise": "enterprise", 129 | "Epoch Cassette Vision": "epoch-cassette-vision", 130 | "Epoch Game Pocket Computer": "epoch-game-pocket-computer", 131 | "Epoch Super Cassette Vision": "epoch-super-cassette-vision", 132 | "Evercade": "evercade", 133 | "ExEn": "exen", 134 | "Exelvision": "exelvision", 135 | "Exidy Sorcerer": "exidy-sorcerer", 136 | "FM Towns": "fm-towns", 137 | "FM-7": "fm-7", 138 | "Fairchild Channel F": "fairchild-channel-f", 139 | "Family Computer": "famicom", 140 | "Family Computer Disk System": "fds", 141 | "Feature phone": "mobile-custom", 142 | "Ferranti Nimrod Computer": "nimrod", 143 | "Freebox": "freebox", 144 | "G-cluster": "g-cluster", 145 | "GIMINI": "gimini", 146 | "GNEX": "gnex", 147 | "GP2X": "gp2x", 148 | "GP2X Wiz": "gp2x-wiz", 149 | "GP32": "gp32", 150 | "GVM": "gvm", 151 | "Galaksija": "galaksija", 152 | "Gamate": "gamate", 153 | "Game & Watch": "g-and-w", 154 | "Game Boy": "gb", 155 | "Game Boy Advance": "gba", 156 | "Game Boy Color": "gbc", 157 | "Game Master": "hartung", 158 | "Game Wave": "game-wave", 159 | "Game.com": "game-dot-com", 160 | "GameStick": "gamestick", 161 | "Gear VR": "gear-vr", 162 | "Gizmondo": "gizmondo", 163 | "Gloud": "gloud", 164 | "Glulx": "glulx", 165 | "Google Stadia": "stadia", 166 | "HD DVD Player": "hd-dvd-player", 167 | "HP 2100": "hp2100", 168 | "HP 3000": "hp3000", 169 | "HP 9800": "hp-9800", 170 | "HP Programmable Calculator": "hp-programmable-calculator", 171 | "Handheld Electronic LCD": "handheld-electronic-lcd", 172 | "Heath/Zenith H8/H89": "heathzenith", 173 | "Heathkit H11": "heathkit-h11", 174 | "Hector HRX": "hrx", 175 | "Hitachi S1": "hitachi-s1", 176 | "Hugo": "hugo", 177 | "Hyper Neo Geo 64": "hyper-neo-geo-64", 178 | "HyperScan": "hyperscan", 179 | "IBM 5100": "ibm-5100", 180 | "IBM PCjr": "pc-jr", 181 | "Ideal-Computer": "ideal-computer", 182 | "Imlac PDS-1": "imlac-pds1", 183 | "Intel 8008": "intel-8008", 184 | "Intel 8080": "intel-8080", 185 | "Intel 8086 / 8088": "intel-8086", 186 | "Intellivision": "intellivision", 187 | "Intellivision Amico": "intellivision-amico", 188 | "Interact Model One": "interact-model-one", 189 | "Interton VC 4000": "interton-vc-4000", 190 | "Interton Video 2000": "interton-video-2000", 191 | "J2ME": "j2me", 192 | "Jolt": "jolt", 193 | "Jupiter Ace": "jupiter-ace", 194 | "KIM-1": "kim-1", 195 | "KaiOS": "kaios", 196 | "Kindle Classic": "kindle", 197 | "Laser 200": "laser200", 198 | "LaserActive": "laseractive", 199 | "LeapFrog Explorer": "leapfrog-explorer", 200 | "LeapTV": "leaptv", 201 | "Leapster": "leapster", 202 | "Leapster Explorer/LeadPad Explorer": "leapster-explorer-slash-leadpad-explorer", 203 | "Legacy Computer": "legacy-computer", 204 | "Legacy Mobile Device": "mobile", 205 | "Linux": "linux", 206 | "Luna": "luna", 207 | "MOS Technology 6502": "mos-technology-6502", 208 | "MRE": "mre", 209 | "MSX": "msx", 210 | "MSX Turbo R": "msx-turbo", 211 | "MSX2": "msx2", 212 | "MUGEN": "mugen", 213 | "Mac": "mac", 214 | "Maemo": "maemo", 215 | "Magnavox Odyssey": "odyssey", 216 | "Mainframe": "mainframe", 217 | "Matsushita/Panasonic JR": "matsushitapanasonic-jr", 218 | "Mattel Aquarius": "aquarius", 219 | "MeeGo": "meego", 220 | "Mega Duck/Cougar Boy": "mega-duck-slash-cougar-boy", 221 | "Memotech MTX": "memotech-mtx", 222 | "Memotech MTX512": "mtx512", 223 | "Meritum": "meritum", 224 | "Meta Quest 2": "meta-quest-2", 225 | "Meta Quest 3": "meta-quest-3", 226 | "Microbee": "microbee", 227 | "Microcomputer": "microcomputer", 228 | "Microsoft MSX2+": "msx2plus", 229 | "Microtan 65": "microtan-65", 230 | "Microvision": "microvision", 231 | "Mophun": "mophun", 232 | "Motorola 6800": "motorola-6800", 233 | "Motorola 68k": "motorola-68k", 234 | "N-Gage": "ngage", 235 | "N-Gage (service)": "ngage2", 236 | "NEC PC-6000 Series": "nec-pc-6000-series", 237 | "Namco System 22": "system-32", 238 | "Nascom": "nascom", 239 | "Neo Geo AES": "neogeoaes", 240 | "Neo Geo CD": "neo-geo-cd", 241 | "Neo Geo MVS": "neogeomvs", 242 | "Neo Geo Pocket": "neo-geo-pocket", 243 | "Neo Geo Pocket Color": "neo-geo-pocket-color", 244 | "Neo Geo X": "neo-geo-x", 245 | "New Nintendo 3DS": "new-nintendo-3ds", 246 | "NewBrain": "newbrain", 247 | "Newton": "newton", 248 | "Nintendo 3DS": "3ds", 249 | "Nintendo 64": "n64", 250 | "Nintendo 64DD": "64dd", 251 | "Nintendo DS": "nds", 252 | "Nintendo DSi": "nintendo-dsi", 253 | "Nintendo Entertainment System": "nes", 254 | "Nintendo GameCube": "ngc", 255 | "Nintendo Switch": "switch", 256 | "Nintendo Switch 2": "switch-2", 257 | "North Star": "northstar", 258 | "Noval 760": "noval-760", 259 | "Nuon": "nuon", 260 | "OOParts": "ooparts", 261 | "OS/2": "os2", 262 | "Oculus Go": "oculus-go", 263 | "Oculus Quest": "oculus-quest", 264 | "Oculus Rift": "oculus-rift", 265 | "Oculus VR": "oculus-vr", 266 | "Odyssey 2": "odyssey-2", 267 | "Odyssey 2 / Videopac G7000": "odyssey-2-slash-videopac-g7000", 268 | "Ohio Scientific": "ohio-scientific", 269 | "OnLive Game System": "onlive-game-system", 270 | "OpenBOR": "openbor", 271 | "Orao": "orao", 272 | "Oric": "oric", 273 | "Oric Atmos": "atmos", 274 | "Othello Multivision": "multivision", 275 | "Ouya": "ouya", 276 | "PC Booter": "pc-booter", 277 | "PC Engine SuperGrafx": "supergrafx", 278 | "PC-50X Family": "pc-50x-family", 279 | "PC-6001": "pc-6001", 280 | "PC-8000": "pc-8000", 281 | "PC-8800 Series": "pc-8800-series", 282 | "PC-9800 Series": "pc-9800-series", 283 | "PC-FX": "pc-fx", 284 | "PDP-1": "pdp1", 285 | "PDP-10": "pdp10", 286 | "PDP-11": "pdp11", 287 | "PDP-7": "pdp-7", 288 | "PDP-8": "pdp-8", 289 | "PICO": "pico", 290 | "PLATO": "plato", 291 | "Palm OS": "palm-os", 292 | "Palmtex": "palmtex", 293 | "Panasonic Jungle": "panasonic-jungle", 294 | "Panasonic M2": "panasonic-m2", 295 | "Pandora": "pandora", 296 | "Pebble": "pebble", 297 | "Philips CD-i": "philips-cd-i", 298 | "Philips VG 5000": "philips-vg-5000", 299 | "Photo CD": "photocd", 300 | "Pinball": "pinball", 301 | "Pippin": "pippin", 302 | "PlayStation": "psx", 303 | "PlayStation 2": "ps2", 304 | "PlayStation 3": "ps3", 305 | "PlayStation 4": "ps4", 306 | "PlayStation 5": "ps5", 307 | "PlayStation Now": "playstation-now", 308 | "PlayStation Portable": "psp", 309 | "PlayStation VR": "psvr", 310 | "PlayStation VR2": "psvr2", 311 | "PlayStation Vita": "psvita", 312 | "Playdate": "playdate", 313 | "Playdia": "playdia", 314 | "Plex Arcade": "plex-arcade", 315 | "Plug & Play": "plug-and-play", 316 | "PocketStation": "pocketstation", 317 | "Pokitto": "pokitto", 318 | "Pokémon mini": "pokemon-mini", 319 | "Poly-88": "poly-88", 320 | "Polymega": "polymega", 321 | "R-Zone": "r-zone", 322 | "RCA Studio II": "rca-studio-ii", 323 | "Research Machines 380Z": "research-machines-380z", 324 | "Roku": "roku", 325 | "SAM Coupé": "sam-coupe", 326 | "SC/MP": "scmp", 327 | "SD-200/270/290": "sd-200270290", 328 | "SDS Sigma 7": "sdssigma7", 329 | "SG-1000": "sg1000", 330 | "SK-VM": "sk-vm", 331 | "SMC-777": "smc-777", 332 | "SRI-500/1000": "sri-5001000", 333 | "SWTPC 6800": "swtpc-6800", 334 | "Satellaview": "satellaview", 335 | "ScummVM": "scummvm", 336 | "Sega 32X": "sega32", 337 | "Sega Advanced Pico Beena": "beena", 338 | "Sega CD": "segacd", 339 | "Sega CD 32X": "segacd32", 340 | "Sega Dreamcast VMU": "vmu", 341 | "Sega Game Gear": "gamegear", 342 | "Sega Hikaru": "hikaru", 343 | "Sega Master System/Mark III": "sms", 344 | "Sega Mega Drive/Genesis": "genesis", 345 | "Sega Model 1": "model1", 346 | "Sega Model 2": "model2", 347 | "Sega Model 3": "model3", 348 | "Sega Pico": "sega-pico", 349 | "Sega SC-3000": "sc3000", 350 | "Sega ST-V": "stv", 351 | "Sega Saturn": "saturn", 352 | "Sega System 16": "system16", 353 | "Sega System 32": "system32", 354 | "Sharp MZ-2200": "sharp-mz-2200", 355 | "Sharp MZ-80B/2000/2500": "sharp-mz-80b20002500", 356 | "Sharp MZ-80K/700/800/1500": "sharp-mz-80k7008001500", 357 | "Sharp X1": "x1", 358 | "Sharp X68000": "sharp-x68000", 359 | "Sharp Zaurus": "sharp-zaurus", 360 | "Signetics 2650": "signetics-2650", 361 | "Sinclair QL": "sinclair-ql", 362 | "Sinclair ZX81": "zx81", 363 | "Socrates": "socrates", 364 | "Sol-20": "sol-20", 365 | "Sony PSP Minis": "psp-minis", 366 | "Sord M5": "sord-m5", 367 | "Spectravideo": "spectravideo", 368 | "SteamVR": "steam-vr", 369 | "Sufami Turbo": "sufami-turbo", 370 | "Super A'Can": "super-acan", 371 | "Super Famicom": "sfam", 372 | "Super NES CD-ROM System": "super-nes-cd-rom-system", 373 | "Super Nintendo Entertainment System": "snes", 374 | "Super Vision 8000": "super-vision-8000", 375 | "Sure Shot HD": "sure-shot-hd", 376 | "SwanCrystal": "swancrystal", 377 | "Symbian": "symbian", 378 | "TADS": "tads", 379 | "TI Programmable Calculator": "ti-programmable-calculator", 380 | "TI-99/4A": "ti-994a", 381 | "TIM": "tim", 382 | "TRS-80": "trs-80", 383 | "TRS-80 Color Computer": "trs-80-color-computer", 384 | "TRS-80 MC-10": "trs-80-mc-10", 385 | "TRS-80 Model 100": "trs-80-model-100", 386 | "Taito Type X": "type-x", 387 | "Taito X-55": "taito-x-55", 388 | "Tandy Vis": "tandy-vis", 389 | "Tapwave Zodiac": "zod", 390 | "Tatung Einstein": "tatung-einstein", 391 | "Tektronix 4050": "tektronix-4050", 392 | "Tele-Spiel ES-2201": "tele-spiel", 393 | "Telstar Arcade": "telstar-arcade", 394 | "Terebikko / See 'n Say Video Phone": "terebikko-slash-see-n-say-video-phone", 395 | "Terminal": "terminal", 396 | "Texas Instruments TI-82": "ti-82", 397 | "Texas Instruments TI-83": "ti-83", 398 | "Texas Instruments TI-99": "ti-99", 399 | "Thomson MO5": "thomson-mo5", 400 | "Thomson TO": "thomson-to", 401 | "Tiki 100": "tiki-100", 402 | "Timex Sinclair 2068": "timex-sinclair-2068", 403 | "Tizen": "tizen", 404 | "Tomahawk F1": "tomahawk-f1", 405 | "Tomy Tutor": "tomy-tutor", 406 | "Tomy Tutor / Pyuta / Grandstand Tutor": "tomy-tutor-slash-pyuta-slash-grandstand-tutor", 407 | "Triton": "triton", 408 | "TurboGrafx-16/PC Engine": "tg16", 409 | "Turbografx-16/PC Engine CD": "turbografx-cd", 410 | "Uzebox": "uzebox", 411 | "V.Flash": "vflash", 412 | "V.Smile": "vsmile", 413 | "VC 4000": "vc-4000", 414 | "VIS": "vis", 415 | "Vector-06C": "06c", 416 | "Vectrex": "vectrex", 417 | "Versatile": "versatile", 418 | "VideoBrain": "videobrain", 419 | "Videopac+ G7400": "videopac-g7400", 420 | "Virtual Boy": "virtualboy", 421 | "Virtual Console": "vc", 422 | "Visual Memory Unit / Visual Memory System": "visual-memory-unit-slash-visual-memory-system", 423 | "WASM-4": "wasm-4", 424 | "WIPI": "wipi", 425 | "Wang 2200": "wang2200", 426 | "Watara/QuickShot Supervision": "supervision", 427 | "Wii": "wii", 428 | "Wii U": "wiiu", 429 | "Windows": "win", 430 | "Windows 3.x": "win3x", 431 | "Windows Apps": "windows-apps", 432 | "Windows Mixed Reality": "windows-mixed-reality", 433 | "Windows Mobile": "windows-mobile", 434 | "Windows Phone": "winphone", 435 | "WoW Action Max": "action-max", 436 | "WonderSwan": "wonderswan", 437 | "WonderSwan Color": "wonderswan-color", 438 | "XaviXPORT": "xavixport", 439 | "Xbox": "xbox", 440 | "Xbox 360": "xbox360", 441 | "Xbox Cloud Gaming": "xboxcloudgaming", 442 | "Xbox One": "xboxone", 443 | "Xbox Series X": "series-x-s", 444 | "Xerox Alto": "xerox-alto", 445 | "Z-machine": "z-machine", 446 | "ZX Spectrum": "zxs", 447 | "ZX Spectrum Next": "zx-spectrum-next", 448 | "ZX80": "zx80", 449 | "Zeebo": "zeebo", 450 | "ZiNc": "zinc", 451 | "Zilog Z80": "z80", 452 | "Zilog Z8000": "zilog-z8000", 453 | "Zodiac": "zodiac", 454 | "Zune": "zune", 455 | "e-Reader / Card-e Reader": "e-reader-slash-card-e-reader", 456 | "iOS": "ios", 457 | "iPad": "ipad", 458 | "iPod Classic": "ipod-classic", 459 | "iiRcade": "iircade", 460 | "tvOS": "tvos", 461 | "visionOS": "visionos", 462 | "watchOS": "watchos", 463 | "webOS": "webos", 464 | } 465 | 466 | # Common aliases and alternative names 467 | PLATFORM_ALIASES = { 468 | "3DO": "3do", 469 | "3DS": "3ds", 470 | "Amiga": "amiga", 471 | "Arcade": "arcade", 472 | "Atari 2600": "atari2600", 473 | "Atari 5200": "atari5200", 474 | "Atari 7800": "atari7800", 475 | "C64": "c64", 476 | "Commodore 64": "c64", 477 | "DOS": "dos", 478 | "DS": "nds", 479 | "Dreamcast": "dc", 480 | "Famicom": "famicom", 481 | "GBA": "gba", 482 | "GBC": "gbc", 483 | "Game Boy": "gb", 484 | "Game Gear": "gamegear", 485 | "GameCube": "ngc", 486 | "Genesis": "genesis", 487 | "Jaguar": "jaguar", 488 | "Linux": "linux", 489 | "Lynx": "lynx", 490 | "MAME": "arcade", 491 | "Mac": "mac", 492 | "Master System": "sms", 493 | "Mega Drive": "genesis", 494 | "N64": "n64", 495 | "NES": "nes", 496 | "NGP": "neo-geo-pocket", 497 | "NGPC": "neo-geo-pocket-color", 498 | "Neo Geo": "neogeoaes", 499 | "Neo Geo AES": "neogeoaes", 500 | "Neo Geo MVS": "neogeomvs", 501 | "Neo Geo Pocket": "neo-geo-pocket", 502 | "PC": "win", 503 | "PC Engine": "tg16", 504 | "PCE": "tg16", 505 | "PS Vita": "psvita", 506 | "PS1": "psx", 507 | "PS2": "ps2", 508 | "PS3": "ps3", 509 | "PS4": "ps4", 510 | "PS5": "ps5", 511 | "PSP": "psp", 512 | "PlayStation": "psx", 513 | "SNES": "snes", 514 | "Saturn": "saturn", 515 | "Spectrum": "zxs", 516 | "Super Famicom": "sfam", 517 | "Switch": "switch", 518 | "TG16": "tg16", 519 | "TurboGrafx-16": "tg16", 520 | "Vita": "psvita", 521 | "Wii": "wii", 522 | "Wii U": "wiiu", 523 | "Windows": "win", 524 | "Xbox": "xbox", 525 | "Xbox 360": "xbox360", 526 | "Xbox One": "xboxone", 527 | "ZX Spectrum": "zxs", 528 | "macOS": "mac", 529 | } 530 | -------------------------------------------------------------------------------- /decky_plugin/main.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import json 3 | import logging 4 | import os 5 | from pathlib import Path 6 | 7 | # Set up logging 8 | log_file = Path.home() / '.config' / 'romm-retroarch-sync' / 'decky_debug.log' 9 | log_file.parent.mkdir(parents=True, exist_ok=True) 10 | settings_file = Path.home() / '.config' / 'romm-retroarch-sync' / 'decky_settings.json' 11 | 12 | # Helper function to load settings 13 | def load_decky_settings(): 14 | """Load decky plugin settings""" 15 | try: 16 | if settings_file.exists(): 17 | with open(settings_file, 'r') as f: 18 | return json.load(f) 19 | except Exception as e: 20 | print(f"Failed to load decky settings: {e}") 21 | return {'logging_enabled': True} # Default to logging enabled 22 | 23 | # Helper function to save settings 24 | def save_decky_settings(settings): 25 | """Save decky plugin settings""" 26 | try: 27 | settings_file.parent.mkdir(parents=True, exist_ok=True) 28 | with open(settings_file, 'w') as f: 29 | json.dump(settings, f, indent=2) 30 | return True 31 | except Exception as e: 32 | print(f"Failed to save decky settings: {e}") 33 | return False 34 | 35 | # Load settings and configure logging 36 | decky_settings = load_decky_settings() 37 | logging_enabled = decky_settings.get('logging_enabled', True) 38 | 39 | if logging_enabled: 40 | logging.basicConfig( 41 | filename=str(log_file), 42 | level=logging.DEBUG, 43 | format='%(asctime)s - %(levelname)s - %(message)s' 44 | ) 45 | else: 46 | # Disable logging by setting level to CRITICAL (effectively silences DEBUG/INFO) 47 | logging.basicConfig( 48 | level=logging.CRITICAL + 1, 49 | format='%(asctime)s - %(levelname)s - %(message)s' 50 | ) 51 | 52 | def get_systemd_env(): 53 | """Get proper environment variables for systemd user commands on Steam Deck""" 54 | env = os.environ.copy() 55 | 56 | # Ensure XDG_RUNTIME_DIR is set (required for systemd --user) 57 | if 'XDG_RUNTIME_DIR' not in env: 58 | uid = os.getuid() 59 | env['XDG_RUNTIME_DIR'] = f'/run/user/{uid}' 60 | 61 | # Try to get DBUS_SESSION_BUS_ADDRESS if not set 62 | if 'DBUS_SESSION_BUS_ADDRESS' not in env: 63 | xdg_runtime = env.get('XDG_RUNTIME_DIR', f'/run/user/{os.getuid()}') 64 | bus_path = Path(xdg_runtime) / 'bus' 65 | if bus_path.exists(): 66 | env['DBUS_SESSION_BUS_ADDRESS'] = f'unix:path={bus_path}' 67 | 68 | return env 69 | 70 | class Plugin: 71 | async def _main(self): 72 | logging.info("RomM Sync Monitor starting...") 73 | return await self.get_service_status() 74 | 75 | async def get_service_status(self): 76 | """Check if the RomM sync service is running""" 77 | try: 78 | # Get proper environment for systemd commands 79 | env = get_systemd_env() 80 | 81 | # Check systemd service status with proper environment 82 | logging.debug(f"Checking service with env: XDG_RUNTIME_DIR={env.get('XDG_RUNTIME_DIR')}") 83 | result = subprocess.run( 84 | ['systemctl', '--user', 'is-active', 'romm-retroarch-sync.service'], 85 | capture_output=True, 86 | text=True, 87 | env=env 88 | ) 89 | 90 | logging.debug(f"systemctl returncode: {result.returncode}, stdout: {result.stdout.strip()}, stderr: {result.stderr.strip()}") 91 | 92 | service_running = result.returncode == 0 and 'active' in result.stdout 93 | 94 | # Fallback: Check if process is running by looking for the status file being recently updated 95 | status_file = Path.home() / '.config' / 'romm-retroarch-sync' / 'status.json' 96 | app_status = {} 97 | status_file_recent = False 98 | 99 | if status_file.exists(): 100 | try: 101 | with open(status_file, 'r') as f: 102 | app_status = json.load(f) 103 | logging.info(f"Status file read: {app_status}") 104 | 105 | # Check if status file was updated recently (within last 120 seconds) 106 | # Increased timeout to handle large collection sync calculations 107 | import time 108 | file_mtime = status_file.stat().st_mtime 109 | status_file_recent = (time.time() - file_mtime) < 300 110 | logging.debug(f"Status file age: {time.time() - file_mtime}s, recent: {status_file_recent}") 111 | except Exception as e: 112 | logging.error(f"Failed to read status file: {e}") 113 | 114 | # If systemctl failed but status file is recent and shows running, trust the status file 115 | # This handles cases where systemctl --user doesn't work properly on Steam Deck 116 | if not service_running and status_file_recent and app_status.get('running', False): 117 | logging.info("systemctl check failed but status file indicates service is running") 118 | service_running = True 119 | 120 | # Combine service and app status 121 | if service_running: 122 | if app_status.get('connected', False): 123 | # Build message with game count and collections 124 | game_count = app_status.get('game_count', 0) 125 | collections = app_status.get('collections', []) 126 | collection_count = app_status.get('collection_count', 0) 127 | actively_syncing_count = app_status.get('actively_syncing_count', 0) 128 | 129 | message = f"Connected ({game_count} games" 130 | if collection_count > 0: 131 | message += f", {collection_count} collections" 132 | message += ")" 133 | 134 | # Log collection data for debugging 135 | for col in collections: 136 | logging.info(f"[STATUS] Collection {col.get('name')}: auto_sync={col.get('auto_sync')}, downloaded={col.get('downloaded')}, total={col.get('total')}, sync_state={col.get('sync_state')}") 137 | 138 | return { 139 | 'status': 'connected', 140 | 'message': message, 141 | 'details': app_status, 142 | 'collections': collections, 143 | 'collection_count': collection_count, 144 | 'actively_syncing_count': actively_syncing_count 145 | } 146 | elif app_status.get('running', False): 147 | return { 148 | 'status': 'running', 149 | 'message': "Running (not connected)", 150 | 'details': app_status, 151 | 'collections': app_status.get('collections', []), 152 | 'collection_count': app_status.get('collection_count', 0), 153 | 'actively_syncing_count': app_status.get('actively_syncing_count', 0) 154 | } 155 | else: 156 | return { 157 | 'status': 'service_only', 158 | 'message': "Service active", 159 | 'details': {}, 160 | 'collections': [], 161 | 'collection_count': 0 162 | } 163 | else: 164 | return { 165 | 'status': 'stopped', 166 | 'message': "Service stopped", 167 | 'details': {}, 168 | 'collections': [], 169 | 'collection_count': 0 170 | } 171 | 172 | except Exception as e: 173 | logging.error(f"Status check error: {e}", exc_info=True) 174 | return { 175 | 'status': 'error', 176 | 'message': f"Error: {str(e)[:50]}", 177 | 'details': {}, 178 | 'collections': [], 179 | 'collection_count': 0 180 | } 181 | 182 | async def start_service(self): 183 | """Start the RomM sync service""" 184 | try: 185 | env = get_systemd_env() 186 | result = subprocess.run( 187 | ['systemctl', '--user', 'start', 'romm-retroarch-sync.service'], 188 | capture_output=True, 189 | text=True, 190 | env=env 191 | ) 192 | logging.info(f"start_service returncode: {result.returncode}, stderr: {result.stderr}") 193 | return result.returncode == 0 194 | except Exception as e: 195 | logging.error(f"start_service error: {e}") 196 | return False 197 | 198 | async def stop_service(self): 199 | """Stop the RomM sync service""" 200 | try: 201 | env = get_systemd_env() 202 | result = subprocess.run( 203 | ['systemctl', '--user', 'stop', 'romm-retroarch-sync.service'], 204 | capture_output=True, 205 | text=True, 206 | env=env 207 | ) 208 | logging.info(f"stop_service returncode: {result.returncode}, stderr: {result.stderr}") 209 | return result.returncode == 0 210 | except Exception as e: 211 | logging.error(f"stop_service error: {e}") 212 | return False 213 | 214 | async def toggle_collection_sync(self, collection_name: str, enabled: bool): 215 | """Enable or disable auto-sync for a specific collection""" 216 | try: 217 | import configparser 218 | from pathlib import Path 219 | 220 | settings_file = Path.home() / '.config' / 'romm-retroarch-sync' / 'settings.ini' 221 | 222 | if not settings_file.exists(): 223 | logging.error("Settings file not found") 224 | return False 225 | 226 | config = configparser.ConfigParser() 227 | config.read(settings_file) 228 | 229 | # Ensure Collections section exists 230 | if not config.has_section('Collections'): 231 | config.add_section('Collections') 232 | 233 | # Get current actively syncing collections 234 | actively_syncing = config.get('Collections', 'actively_syncing', fallback='') 235 | sync_set = set(actively_syncing.split('|')) if actively_syncing else set() 236 | sync_set.discard('') # Remove empty strings 237 | 238 | # Toggle the collection 239 | if enabled: 240 | sync_set.add(collection_name) 241 | logging.info(f"Enabling auto-sync for collection: {collection_name}") 242 | else: 243 | sync_set.discard(collection_name) 244 | logging.info(f"Disabling auto-sync for collection: {collection_name}") 245 | 246 | # Update settings 247 | config.set('Collections', 'actively_syncing', '|'.join(sorted(sync_set))) 248 | 249 | # Also update selected_for_sync - daemon uses this field! 250 | config.set('Collections', 'selected_for_sync', '|'.join(sorted(sync_set))) 251 | 252 | # Enable auto_sync_enabled if at least one collection is syncing 253 | if sync_set: 254 | config.set('Collections', 'auto_sync_enabled', 'true') 255 | else: 256 | config.set('Collections', 'auto_sync_enabled', 'false') 257 | 258 | # Write back to file 259 | with open(settings_file, 'w') as f: 260 | config.write(f) 261 | 262 | logging.info(f"Updated settings: actively_syncing={config.get('Collections', 'actively_syncing')}") 263 | 264 | # Trigger immediate daemon reload by creating a flag file 265 | try: 266 | reload_flag = Path.home() / '.config' / 'romm-retroarch-sync' / '.reload_trigger' 267 | reload_flag.touch() 268 | logging.info(f"✅ Created reload trigger file: {reload_flag}") 269 | except Exception as e: 270 | logging.error(f"Error creating reload trigger: {e}", exc_info=True) 271 | 272 | return True 273 | 274 | except Exception as e: 275 | logging.error(f"toggle_collection_sync error: {e}", exc_info=True) 276 | return False 277 | 278 | async def delete_collection_roms(self, collection_name: str): 279 | """Delete all ROMs for a specific collection to allow re-sync from scratch""" 280 | try: 281 | import configparser 282 | logging.info(f"🗑️ Starting ROM deletion for collection: {collection_name}") 283 | 284 | # Load settings to get download directory 285 | settings_file = Path.home() / '.config' / 'romm-retroarch-sync' / 'settings.ini' 286 | if not settings_file.exists(): 287 | logging.error("Settings file not found") 288 | return False 289 | 290 | config = configparser.ConfigParser() 291 | config.read(settings_file) 292 | 293 | # Get download directory (from Download section, fallback to ~/retrodeck/roms) 294 | download_dir = config.get('Download', 'rom_directory', fallback='~/retrodeck/roms') 295 | download_dir = Path(download_dir).expanduser() 296 | 297 | if not download_dir.exists(): 298 | logging.error(f"Download directory not found: {download_dir}") 299 | return False 300 | 301 | # Read status file to get collection ID 302 | status_file = Path.home() / '.config' / 'romm-retroarch-sync' / 'status.json' 303 | if not status_file.exists(): 304 | logging.error("Status file not found") 305 | return False 306 | 307 | with open(status_file, 'r') as f: 308 | status_data = json.load(f) 309 | 310 | # Find the collection by name 311 | collection_id = None 312 | for col in status_data.get('collections', []): 313 | if col['name'] == collection_name: 314 | collection_id = col['id'] 315 | break 316 | 317 | if collection_id is None: 318 | logging.error(f"Collection '{collection_name}' not found in status") 319 | return False 320 | 321 | # Use urllib (standard library) to fetch collection ROMs from RomM API 322 | import urllib.request 323 | import urllib.error 324 | import base64 325 | 326 | # Get RomM credentials 327 | romm_url = config.get('RomM', 'url', fallback='') 328 | username = config.get('RomM', 'username', fallback='') 329 | password = config.get('RomM', 'password', fallback='') 330 | 331 | if not all([romm_url, username, password]): 332 | logging.error("Missing RomM credentials in settings") 333 | return False 334 | 335 | # Fetch collection ROMs using urllib 336 | api_url = f"{romm_url.rstrip('/')}/api/roms?collection_id={collection_id}" 337 | logging.info(f"Fetching ROMs from: {api_url}") 338 | 339 | try: 340 | # Create SSL context that doesn't verify certificates (like the daemon does) 341 | import ssl 342 | ssl_context = ssl.create_default_context() 343 | ssl_context.check_hostname = False 344 | ssl_context.verify_mode = ssl.CERT_NONE 345 | 346 | # Create request with Basic Auth 347 | credentials = base64.b64encode(f"{username}:{password}".encode()).decode() 348 | req = urllib.request.Request(api_url) 349 | req.add_header('Authorization', f'Basic {credentials}') 350 | 351 | with urllib.request.urlopen(req, context=ssl_context) as response: 352 | data = json.loads(response.read().decode()) 353 | collection_roms = data.get('items', []) 354 | 355 | deleted_count = 0 356 | 357 | # Delete each ROM file that belongs to this collection only 358 | for rom in collection_roms: 359 | platform_slug = rom.get('platform_slug', '') 360 | file_name = rom.get('fs_name') or rom.get('file_name', '') 361 | 362 | if platform_slug and file_name: 363 | platform_dir = download_dir / platform_slug 364 | rom_path = platform_dir / file_name 365 | 366 | if rom_path.exists() and rom_path.is_file(): 367 | try: 368 | rom_path.unlink() 369 | deleted_count += 1 370 | logging.info(f" 🗑️ Deleted: {rom_path}") 371 | except Exception as e: 372 | logging.error(f" ❌ Failed to delete {rom_path}: {e}") 373 | 374 | logging.info(f"✅ Deleted {deleted_count} ROM(s) from collection '{collection_name}'") 375 | 376 | except urllib.error.HTTPError as e: 377 | logging.error(f"HTTP error fetching collection ROMs: {e.code} - {e.reason}") 378 | return False 379 | except Exception as e: 380 | logging.error(f"Error fetching collection ROMs: {e}") 381 | return False 382 | 383 | # Disable auto-sync for this collection 384 | if not config.has_section('Collections'): 385 | config.add_section('Collections') 386 | 387 | actively_syncing = config.get('Collections', 'actively_syncing', fallback='') 388 | sync_set = set(actively_syncing.split('|')) if actively_syncing else set() 389 | sync_set.discard('') # Remove empty strings 390 | sync_set.discard(collection_name) # Remove this collection 391 | 392 | # Update settings 393 | config.set('Collections', 'actively_syncing', '|'.join(sorted(sync_set))) 394 | 395 | # Disable auto_sync_enabled if no collections are syncing 396 | if not sync_set: 397 | config.set('Collections', 'auto_sync_enabled', 'false') 398 | 399 | # Write back to file 400 | with open(settings_file, 'w') as f: 401 | config.write(f) 402 | 403 | logging.info(f"🔴 Disabled auto-sync for collection: {collection_name}") 404 | 405 | # Trigger daemon reload 406 | try: 407 | reload_flag = Path.home() / '.config' / 'romm-retroarch-sync' / '.reload_trigger' 408 | reload_flag.touch() 409 | logging.info(f"✅ Created reload trigger file") 410 | except Exception as e: 411 | logging.error(f"Error creating reload trigger: {e}") 412 | 413 | return True 414 | 415 | except Exception as e: 416 | logging.error(f"delete_collection_roms error: {e}", exc_info=True) 417 | return False 418 | 419 | async def get_logging_enabled(self): 420 | """Get current logging preference""" 421 | try: 422 | settings = load_decky_settings() 423 | return settings.get('logging_enabled', True) 424 | except Exception as e: 425 | logging.error(f"get_logging_enabled error: {e}") 426 | return True 427 | 428 | async def set_logging_enabled(self, enabled: bool): 429 | """Set logging preference""" 430 | try: 431 | settings = load_decky_settings() 432 | settings['logging_enabled'] = enabled 433 | result = save_decky_settings(settings) 434 | 435 | if result: 436 | # Update logging level immediately 437 | if enabled: 438 | logging.getLogger().setLevel(logging.DEBUG) 439 | logging.info("✅ Logging enabled") 440 | else: 441 | logging.info("🔴 Logging disabled") 442 | logging.getLogger().setLevel(logging.CRITICAL + 1) 443 | 444 | return result 445 | except Exception as e: 446 | logging.error(f"set_logging_enabled error: {e}") 447 | return False -------------------------------------------------------------------------------- /decky_plugin/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonItem, 3 | PanelSection, 4 | PanelSectionRow, 5 | ToggleField, 6 | Navigation, 7 | staticClasses, 8 | } from "@decky/ui"; 9 | import { callable, definePlugin, toaster, routerHook } from "@decky/api"; 10 | import { useState, useEffect, useRef } from "react"; 11 | import { FaSync, FaTrash, FaCog } from "react-icons/fa"; 12 | 13 | // Call backend methods 14 | const getServiceStatus = callable<[], any>("get_service_status"); 15 | const startService = callable<[], boolean>("start_service"); 16 | const stopService = callable<[], boolean>("stop_service"); 17 | const toggleCollectionSync = callable<[string, boolean], boolean>("toggle_collection_sync"); 18 | const deleteCollectionRoms = callable<[string], boolean>("delete_collection_roms"); 19 | const getLoggingEnabled = callable<[], boolean>("get_logging_enabled"); 20 | const updateLoggingEnabled = callable<[boolean], boolean>("set_logging_enabled"); 21 | 22 | // Background monitoring - runs independently of UI 23 | let backgroundInterval: any = null; 24 | const previousSyncStates = new Map(); 25 | 26 | const checkForNotifications = async () => { 27 | try { 28 | const result = await getServiceStatus(); 29 | 30 | // Collect all collections that need notifications 31 | const notificationsToShow: Array<{name: string, downloaded: number, total: number, type: string}> = []; 32 | 33 | // Detect sync completion 34 | result.collections?.forEach((col: any) => { 35 | const previousData = previousSyncStates.get(col.name); 36 | const currentState = col.sync_state; 37 | 38 | // Log state for debugging 39 | if (col.auto_sync || previousData) { 40 | console.log(`[BACKGROUND CHECK] ${col.name}: prev=${previousData?.sync_state || 'none'}, curr=${currentState}, auto_sync=${col.auto_sync}`); 41 | } 42 | 43 | // Detect transition to 'synced' from either 'syncing' OR 'not_synced' 44 | // 'syncing' -> 'synced': Normal case 45 | // 'not_synced' -> 'synced': Fast sync where we missed the 'syncing' state 46 | if (previousData && currentState === 'synced' && 47 | (previousData.sync_state === 'syncing' || previousData.sync_state === 'not_synced')) { 48 | console.log(`[BACKGROUND NOTIFICATION] Collection '${col.name}' completed syncing: ${col.downloaded}/${col.total} ROMs (prev state: ${previousData.sync_state})`); 49 | notificationsToShow.push({ 50 | name: col.name, 51 | downloaded: col.downloaded, 52 | total: col.total, 53 | type: 'sync' 54 | }); 55 | } 56 | // Also detect when ROM count changes while in 'synced' state (deletions) 57 | else if ( 58 | previousData?.sync_state === 'synced' && 59 | currentState === 'synced' && 60 | col.auto_sync && 61 | previousData.downloaded !== undefined && 62 | col.downloaded !== undefined && 63 | previousData.downloaded !== col.downloaded 64 | ) { 65 | console.log(`[BACKGROUND NOTIFICATION] Collection '${col.name}' updated: ${col.downloaded}/${col.total} ROMs (was ${previousData.downloaded}/${previousData.total})`); 66 | notificationsToShow.push({ 67 | name: col.name, 68 | downloaded: col.downloaded, 69 | total: col.total, 70 | type: 'update' 71 | }); 72 | } 73 | 74 | // Update previous state and counts 75 | previousSyncStates.set(col.name, { 76 | sync_state: currentState, 77 | downloaded: col.downloaded, 78 | total: col.total 79 | }); 80 | }); 81 | 82 | // Show notifications with slight delay between each to prevent overlapping/deduplication 83 | for (let i = 0; i < notificationsToShow.length; i++) { 84 | const notification = notificationsToShow[i]; 85 | // Add delay only if not the first notification 86 | if (i > 0) { 87 | await new Promise(resolve => setTimeout(resolve, 300)); 88 | } 89 | console.log(`[BACKGROUND] Showing notification for ${notification.name}`); 90 | toaster.toast({ 91 | title: `✅ ${notification.name} - Sync Complete`, 92 | body: `${notification.downloaded}/${notification.total} ROMs synced`, 93 | duration: 5000, 94 | }); 95 | } 96 | 97 | if (notificationsToShow.length > 0) { 98 | console.log(`[BACKGROUND] Showed ${notificationsToShow.length} notification(s)`); 99 | } 100 | } catch (error) { 101 | console.error('[BACKGROUND NOTIFICATION] Error checking status:', error); 102 | } 103 | }; 104 | 105 | const startBackgroundMonitoring = () => { 106 | if (backgroundInterval) { 107 | clearInterval(backgroundInterval); 108 | } 109 | console.log('[BACKGROUND] Starting background notification monitoring'); 110 | backgroundInterval = setInterval(checkForNotifications, 2000); 111 | }; 112 | 113 | const stopBackgroundMonitoring = () => { 114 | if (backgroundInterval) { 115 | console.log('[BACKGROUND] Stopping background notification monitoring'); 116 | clearInterval(backgroundInterval); 117 | backgroundInterval = null; 118 | } 119 | }; 120 | 121 | // Start monitoring immediately when module loads 122 | console.log('[PLUGIN INIT] Module loaded, starting background monitoring'); 123 | startBackgroundMonitoring(); 124 | 125 | // Settings page component 126 | function SettingsPage() { 127 | const [loggingEnabled, setLoggingEnabled] = useState(true); 128 | const [loading, setLoading] = useState(true); 129 | 130 | useEffect(() => { 131 | // Load initial logging preference 132 | const loadSettings = async () => { 133 | try { 134 | const enabled = await getLoggingEnabled(); 135 | setLoggingEnabled(enabled); 136 | } catch (error) { 137 | console.error('Failed to load logging preference:', error); 138 | } finally { 139 | setLoading(false); 140 | } 141 | }; 142 | loadSettings(); 143 | }, []); 144 | 145 | const handleLoggingToggle = async (enabled: boolean) => { 146 | setLoggingEnabled(enabled); 147 | try { 148 | await updateLoggingEnabled(enabled); 149 | } catch (error) { 150 | console.error('Failed to set logging preference:', error); 151 | // Revert on error 152 | setLoggingEnabled(!enabled); 153 | } 154 | }; 155 | 156 | return ( 157 |
158 |
RomM Sync Settings
159 | 160 | 161 | 168 | 169 | 170 |
171 | ); 172 | } 173 | 174 | function Content() { 175 | const [status, setStatus] = useState({ status: 'loading', message: 'Loading...' }); 176 | const [loading, setLoading] = useState(false); 177 | const [togglingCollection, setTogglingCollection] = useState(null); 178 | const intervalRef = useRef(null); 179 | const optimisticOverrides = useRef>(new Map()); 180 | 181 | const refreshStatus = async () => { 182 | try { 183 | const result = await getServiceStatus(); 184 | console.log(`[REFRESH] ========== NEW REFRESH ==========`); 185 | console.log(`[REFRESH] Raw backend data:`, JSON.stringify(result.collections, null, 2)); 186 | console.log(`[REFRESH] Current overrides:`, Array.from(optimisticOverrides.current.entries())); 187 | 188 | // Check if backend data matches any overrides - if so, clear them 189 | if (optimisticOverrides.current.size > 0) { 190 | result.collections.forEach((col: any) => { 191 | const override = optimisticOverrides.current.get(col.name); 192 | if (override) { 193 | console.log(`[REFRESH] Checking ${col.name}: override={auto_sync:${override.auto_sync}, sync_state:${override.sync_state}}, backend={auto_sync:${col.auto_sync}, sync_state:${col.sync_state}, downloaded:${col.downloaded}, total:${col.total}}`); 194 | } 195 | if (override && override.auto_sync === col.auto_sync) { 196 | // Clear override when backend has REAL progress data (not just 0/0) 197 | const backendHasProgress = (col.downloaded !== undefined && col.total !== undefined && col.total > 0); 198 | const shouldClear = backendHasProgress && ( 199 | // Clear if backend state matches override state 200 | override.sync_state === col.sync_state || 201 | // OR clear if backend is 'synced' (sync completed, override no longer needed) 202 | col.sync_state === 'synced' 203 | ); 204 | console.log(`[REFRESH] ${col.name}: shouldClear=${shouldClear} (backendHasProgress=${backendHasProgress}, override.sync_state=${override.sync_state}, col.sync_state=${col.sync_state})`); 205 | if (shouldClear) { 206 | console.log(`[REFRESH] ✅ CLEARING override for ${col.name}`); 207 | optimisticOverrides.current.delete(col.name); 208 | } else { 209 | console.log(`[REFRESH] ⏸️ KEEPING override for ${col.name} (backend has no progress yet)`); 210 | } 211 | } 212 | }); 213 | } 214 | 215 | // Apply remaining optimistic overrides 216 | if (optimisticOverrides.current.size > 0) { 217 | console.log(`[REFRESH] 🎨 Applying ${optimisticOverrides.current.size} override(s) to result`); 218 | const modifiedResult = { 219 | ...result, 220 | collections: result.collections.map((col: any) => { 221 | const override = optimisticOverrides.current.get(col.name); 222 | if (override) { 223 | const before = `{sync_state:${col.sync_state}, downloaded:${col.downloaded}, total:${col.total}}`; 224 | // Apply override fields - if override has downloaded/total, use them 225 | const modified = { 226 | ...col, 227 | auto_sync: override.auto_sync, 228 | sync_state: override.sync_state, 229 | ...(override.downloaded !== undefined ? { downloaded: override.downloaded } : {}), 230 | ...(override.total !== undefined ? { total: override.total } : {}) 231 | }; 232 | const after = `{sync_state:${modified.sync_state}, downloaded:${modified.downloaded}, total:${modified.total}}`; 233 | console.log(`[REFRESH] 🔧 ${col.name}: BEFORE=${before}, AFTER=${after}`); 234 | return modified; 235 | } 236 | return col; 237 | }) 238 | }; 239 | console.log(`[REFRESH] Final data being set to UI:`, JSON.stringify(modifiedResult.collections, null, 2)); 240 | setStatus(modifiedResult); 241 | } else { 242 | console.log(`[REFRESH] No overrides, using raw backend result`); 243 | console.log(`[REFRESH] Final data being set to UI:`, JSON.stringify(result.collections, null, 2)); 244 | setStatus(result); 245 | } 246 | } catch (error) { 247 | setStatus({ status: 'error', message: '❌ Plugin error' }); 248 | } 249 | }; 250 | 251 | const stopPolling = () => { 252 | if (intervalRef.current) { 253 | clearInterval(intervalRef.current); 254 | intervalRef.current = null; 255 | } 256 | }; 257 | 258 | const startPolling = () => { 259 | stopPolling(); 260 | intervalRef.current = setInterval(refreshStatus, 2000); // Poll every 2 seconds for more responsive UI 261 | }; 262 | 263 | useEffect(() => { 264 | refreshStatus(); 265 | startPolling(); 266 | return () => stopPolling(); 267 | }, []); 268 | 269 | const handleStart = async () => { 270 | setLoading(true); 271 | await startService(); 272 | setTimeout(refreshStatus, 1000); // Refresh after 1 second 273 | setLoading(false); 274 | }; 275 | 276 | const handleStop = async () => { 277 | setLoading(true); 278 | await stopService(); 279 | setTimeout(refreshStatus, 1000); 280 | setLoading(false); 281 | }; 282 | 283 | const handleToggleCollection = async (collectionName: string, enabled: boolean) => { 284 | console.log(`[TOGGLE] Starting toggle for ${collectionName}, enabled=${enabled}`); 285 | 286 | // Get current collection state to determine total 287 | const currentCollection = status?.collections.find((c: any) => c.name === collectionName); 288 | const totalRoms = currentCollection?.total; 289 | const hasValidTotal = totalRoms !== undefined && totalRoms > 0; 290 | 291 | // FIRST: Set override BEFORE anything else to protect against concurrent polling 292 | // Only include downloaded: 0, total: X if we have a valid total (> 0) 293 | // Otherwise, let backend fetch the real total from server 294 | optimisticOverrides.current.set(collectionName, { 295 | auto_sync: enabled, 296 | sync_state: enabled ? 'syncing' : 'not_synced', 297 | ...(enabled && hasValidTotal ? { downloaded: 0, total: totalRoms } : {}) 298 | }); 299 | console.log(`[TOGGLE] Set override for ${collectionName} with downloaded=0, total=${hasValidTotal ? totalRoms : 'none'}, map size:`, optimisticOverrides.current.size); 300 | 301 | setTogglingCollection(collectionName); 302 | 303 | // Update UI immediately - change auto_sync and set initial sync_state 304 | setStatus((prevStatus: any) => { 305 | const updatedCollections = prevStatus.collections.map((col: any) => { 306 | if (col.name === collectionName) { 307 | // When enabling, show as syncing (with 0/total if we have valid total) 308 | // When disabling, show as not_synced 309 | if (enabled) { 310 | if (hasValidTotal) { 311 | return { ...col, auto_sync: true, sync_state: 'syncing', downloaded: 0, total: totalRoms }; 312 | } else { 313 | // No valid total yet - just set syncing, let backend populate counts 314 | return { ...col, auto_sync: true, sync_state: 'syncing' }; 315 | } 316 | } else { 317 | return { ...col, auto_sync: false, sync_state: 'not_synced' }; 318 | } 319 | } 320 | return col; 321 | }); 322 | return { 323 | ...prevStatus, 324 | collections: updatedCollections, 325 | actively_syncing_count: updatedCollections.filter((c: any) => c.auto_sync).length 326 | }; 327 | }); 328 | 329 | try { 330 | console.log(`[TOGGLE] Calling backend toggleCollectionSync...`); 331 | const result = await toggleCollectionSync(collectionName, enabled); 332 | console.log(`[TOGGLE] Backend returned:`, result); 333 | // Force immediate refresh to get the fresh status from backend 334 | // The backend immediately updates status.json, so we should read it ASAP 335 | console.log(`[TOGGLE] Forcing immediate refresh after backend call`); 336 | await refreshStatus(); 337 | } catch (error) { 338 | console.error('[TOGGLE] Failed to toggle collection sync:', error); 339 | // Clear override on error and refresh 340 | optimisticOverrides.current.delete(collectionName); 341 | refreshStatus(); 342 | } finally { 343 | setTogglingCollection(null); 344 | } 345 | }; 346 | 347 | const handleDeleteCollection = async (collectionName: string) => { 348 | console.log(`[DELETE] Starting deletion for ${collectionName}`); 349 | setTogglingCollection(collectionName); 350 | 351 | // Store optimistic override 352 | optimisticOverrides.current.set(collectionName, { 353 | auto_sync: false, 354 | sync_state: 'not_synced' 355 | }); 356 | 357 | // Update UI immediately - only change auto_sync 358 | setStatus((prevStatus: any) => { 359 | const updatedCollections = prevStatus.collections.map((col: any) => 360 | col.name === collectionName 361 | ? { ...col, auto_sync: false } 362 | : col 363 | ); 364 | return { 365 | ...prevStatus, 366 | collections: updatedCollections, 367 | actively_syncing_count: updatedCollections.filter((c: any) => c.auto_sync).length 368 | }; 369 | }); 370 | 371 | try { 372 | const result = await deleteCollectionRoms(collectionName); 373 | console.log(`[DELETE] Backend returned:`, result); 374 | // Override will auto-clear when backend data matches (via refreshStatus) 375 | } catch (error) { 376 | console.error('[DELETE] Failed to delete collection ROMs:', error); 377 | optimisticOverrides.current.delete(collectionName); 378 | refreshStatus(); 379 | } finally { 380 | setTogglingCollection(null); 381 | } 382 | }; 383 | 384 | return ( 385 | 386 | 387 |
388 | {status.message} 389 |
390 |
391 | 392 | {status.collections && status.collections.length > 0 && ( 393 | <> 394 | 395 |
396 | Collections: 397 |
398 |
399 | {status.collections.map((collection: any, index: number) => { 400 | // Determine dot color based on sync state 401 | const getDotColor = () => { 402 | if (!collection.auto_sync) return '#6b7280'; // gray - not syncing 403 | switch (collection.sync_state) { 404 | case 'synced': return '#4ade80'; // green - fully synced 405 | case 'syncing': return '#fb923c'; // orange - currently syncing 406 | case 'not_synced': return '#f87171'; // red - not synced 407 | default: return '#6b7280'; // gray - unknown 408 | } 409 | }; 410 | 411 | return ( 412 |
413 | 414 | 417 |
423 | 424 | {collection.name} 425 | {collection.auto_sync && collection.sync_state === 'syncing' ? ' - Syncing' : ''} 426 | 427 |
428 | } 429 | description={(() => { 430 | console.log(`[RENDER] ${collection.name}: auto_sync=${collection.auto_sync}, downloaded=${collection.downloaded}, total=${collection.total}, sync_state=${collection.sync_state}`); 431 | if (collection.auto_sync) { 432 | // Only show counts if total > 0 (valid data from server) 433 | if (collection.downloaded !== undefined && collection.total !== undefined && collection.total > 0) { 434 | return `${collection.downloaded} / ${collection.total} ROMs`; 435 | } 436 | return "Auto-sync enabled"; 437 | } 438 | return "Auto-sync disabled"; 439 | })()} 440 | checked={collection.auto_sync} 441 | onChange={(value: boolean) => handleToggleCollection(collection.name, value)} 442 | disabled={togglingCollection === collection.name} 443 | /> 444 |
445 | 446 | handleDeleteCollection(collection.name)} 449 | disabled={togglingCollection === collection.name} 450 | > 451 |
452 | 453 | Delete ROMs 454 |
455 |
456 |
457 |
458 | ); 459 | })} 460 | 461 | )} 462 | 463 | 468 |
469 | 🔄 470 | Refresh 471 |
472 |
473 |
474 | {status.status === 'stopped' && ( 475 | 476 | 481 |
482 | ▶️ 483 | Start Service 484 |
485 |
486 |
487 | )} 488 | {status.status !== 'stopped' && status.status !== 'error' && ( 489 | 490 | 495 |
496 | ⏹️ 497 | Stop Service 498 |
499 |
500 |
501 | )} 502 | 503 | 504 | { 507 | Navigation.Navigate("/romm-sync-settings"); 508 | Navigation.CloseSideMenus(); 509 | }} 510 | > 511 |
512 | 513 | Settings 514 |
515 |
516 |
517 |
518 | ); 519 | } 520 | 521 | function TitleView() { 522 | const [status, setStatus] = useState({ status: 'loading' }); 523 | 524 | useEffect(() => { 525 | const fetchStatus = async () => { 526 | const result = await getServiceStatus(); 527 | setStatus(result); 528 | }; 529 | fetchStatus(); 530 | const interval = setInterval(fetchStatus, 5000); 531 | return () => clearInterval(interval); 532 | }, []); 533 | 534 | // Status dot colors 535 | const getStatusColor = () => { 536 | switch (status.status) { 537 | case 'connected': return '#4ade80'; // green 538 | case 'running': return '#fbbf24'; // yellow 539 | case 'service_only': return '#60a5fa'; // blue 540 | case 'stopped': return '#f87171'; // red 541 | case 'error': return '#f87171'; // red 542 | default: return '#9ca3af'; // gray 543 | } 544 | }; 545 | 546 | return ( 547 |
548 |
554 | RomM Sync 555 |
556 | ); 557 | } 558 | 559 | export default definePlugin(() => { 560 | // Register the settings route 561 | routerHook.addRoute("/romm-sync-settings", () => , { 562 | exact: true, 563 | }); 564 | 565 | return { 566 | name: "RomM Sync Monitor", 567 | titleView: , 568 | content: , 569 | icon: , 570 | onDismount: () => { 571 | // Stop background monitoring when plugin unloads 572 | console.log('[PLUGIN] onDismount - Stopping background monitoring'); 573 | stopBackgroundMonitoring(); 574 | 575 | // Remove the settings route 576 | routerHook.removeRoute("/romm-sync-settings"); 577 | }, 578 | }; 579 | }); -------------------------------------------------------------------------------- /src/bios_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | BIOS Manager for RomM-RetroArch Sync 4 | Handles BIOS detection, verification, and synchronization 5 | """ 6 | 7 | from pathlib import Path 8 | import hashlib 9 | import json 10 | import threading 11 | from typing import Dict, List, Tuple, Optional 12 | 13 | # Comprehensive BIOS requirements database 14 | BIOS_DATABASE = { 15 | 'Sony - PlayStation': { 16 | 'cores': ['beetle_psx', 'beetle_psx_hw', 'pcsx_rearmed', 'swanstation', 'duckstation'], 17 | 'bios_files': [ 18 | {'file': 'scph5500.bin', 'md5': '8dd7d5296a650fac7319bce665a6a53c', 'region': 'JP', 'required': True}, 19 | {'file': 'scph5501.bin', 'md5': '490f666e1afb15b7362b406ed1cea246', 'region': 'US', 'required': True}, 20 | {'file': 'scph5502.bin', 'md5': '32736f17079d0b2b7024407c39bd3050', 'region': 'EU', 'required': True}, 21 | {'file': 'scph1001.bin', 'md5': '924e392ed05558ffdb115408c263dccf', 'region': 'US', 'required': False}, 22 | {'file': 'scph7001.bin', 'md5': '1e68c231d0896b7eadcad1d7d8e76129', 'region': 'US', 'required': False}, 23 | {'file': 'scph101.bin', 'md5': '6e3735ff4c7dc899ee98981385f6f3d0', 'region': 'US', 'required': False}, 24 | ] 25 | }, 26 | 'Sony - PlayStation 2': { 27 | 'cores': ['pcsx2', 'play'], 28 | 'bios_files': [ 29 | {'file': 'ps2-0230a-20080220.bin', 'required': True, 'desc': 'PS2 USA BIOS'}, 30 | {'file': 'ps2-0230e-20080220.bin', 'required': True, 'desc': 'PS2 Europe BIOS'}, 31 | {'file': 'ps2-0230j-20080220.bin', 'required': True, 'desc': 'PS2 Japan BIOS'}, 32 | {'file': 'SCPH-70012_BIOS_V12_USA_200.BIN', 'required': False}, 33 | {'file': 'SCPH-70004_BIOS_V12_EUR_200.BIN', 'required': False}, 34 | ] 35 | }, 36 | 'Sega - Saturn': { 37 | 'cores': ['beetle_saturn', 'kronos', 'yabause'], 38 | 'bios_files': [ 39 | {'file': 'sega_101.bin', 'md5': '85ec9ca47d8f6807718151cbcca8b964', 'required': True}, 40 | {'file': 'mpr-17933.bin', 'md5': '3240872c70984b6cbfda1586cab68dbe', 'required': True}, 41 | {'file': 'saturn_bios.bin', 'md5': 'af5828fdff51384f99b3c4926be27762', 'required': False}, 42 | ] 43 | }, 44 | 'Sega - Mega-CD - Sega CD': { 45 | 'cores': ['genesis_plus_gx', 'picodrive'], 46 | 'bios_files': [ 47 | {'file': 'bios_CD_U.bin', 'md5': '2efd74e3232ff260e371b99f84024f7f', 'region': 'US', 'required': True}, 48 | {'file': 'bios_CD_E.bin', 'md5': 'e66fa1dc5820d254611fdcdba0662372', 'region': 'EU', 'required': True}, 49 | {'file': 'bios_CD_J.bin', 'md5': '278a9397d192149e84e820ac621a8edd', 'region': 'JP', 'required': True}, 50 | ] 51 | }, 52 | 'Sega - Dreamcast': { 53 | 'cores': ['flycast', 'redream'], 54 | 'bios_files': [ 55 | {'file': 'dc_boot.bin', 'md5': 'e10c53c2f8b90bab96ead2d368858623', 'required': True}, 56 | {'file': 'dc_flash.bin', 'md5': '0a93f7940c455905bea6e392dfde92a4', 'required': True}, 57 | {'file': 'dc_nvmem.bin', 'required': False}, 58 | ] 59 | }, 60 | 'SNK - Neo Geo': { 61 | 'cores': ['fbneo', 'fbalpha', 'mame'], 62 | 'bios_files': [ 63 | {'file': 'neogeo.zip', 'required': True, 'desc': 'Neo Geo BIOS package'}, 64 | {'file': 'uni-bios.rom', 'required': False, 'desc': 'UniBIOS (optional)'}, 65 | ] 66 | }, 67 | 'Nintendo - Nintendo DS': { 68 | 'cores': ['desmume', 'melonds'], 69 | 'bios_files': [ 70 | {'file': 'bios7.bin', 'size': 16384, 'required': True, 'md5': 'df692a80a5b1bc90728bc3dfc76cd948'}, 71 | {'file': 'bios9.bin', 'size': 4096, 'required': True, 'md5': 'a392174eb3e572fed6447e956bde4b25'}, 72 | {'file': 'firmware.bin', 'size': 262144, 'required': True}, 73 | ] 74 | }, 75 | 'Nintendo - Game Boy Advance': { 76 | 'cores': ['mgba', 'vba_next', 'vbam', 'gpsp'], 77 | 'bios_files': [ 78 | {'file': 'gba_bios.bin', 'md5': 'a860e8c0b6d573d191e4ec7db1b1e4f6', 'required': False}, 79 | {'file': 'gb_bios.bin', 'md5': '32fbbd84168d3482956eb3c5051637f5', 'required': False}, 80 | {'file': 'gbc_bios.bin', 'md5': 'dbfce9db9deaa2567f6a84fde55f9680', 'required': False}, 81 | {'file': 'sgb_bios.bin', 'md5': 'd574d4f9c12f305074798f54c091a8b4', 'required': False}, 82 | ] 83 | }, 84 | 'NEC - PC Engine - TurboGrafx 16': { 85 | 'cores': ['beetle_pce', 'beetle_pce_fast', 'beetle_supergrafx'], 86 | 'bios_files': [ 87 | {'file': 'syscard3.pce', 'md5': '38179df8f4ac870017db21ebcbf53114', 'required': True}, 88 | {'file': 'syscard2.pce', 'md5': '3cdd6614a918616bfc41c862e889dd79', 'required': False}, 89 | {'file': 'syscard1.pce', 'md5': '2b7ccb3d86baa18f6402c176f3065082', 'required': False}, 90 | {'file': 'gexpress.pce', 'md5': '51a12d90b2a7a6fbd6509e0a38b1c120', 'required': False}, 91 | ] 92 | }, 93 | 'Atari - 7800': { 94 | 'cores': ['prosystem'], 95 | 'bios_files': [ 96 | {'file': '7800 BIOS (U).rom', 'md5': '0763f1ffb006ddbe32e52d497ee848ae', 'required': False}, 97 | {'file': '7800 BIOS (E).rom', 'required': False}, 98 | ] 99 | }, 100 | 'Atari - Lynx': { 101 | 'cores': ['handy', 'beetle_lynx'], 102 | 'bios_files': [ 103 | {'file': 'lynxboot.img', 'md5': 'fcd403db69f54290b51035d82f835e7b', 'required': True}, 104 | ] 105 | }, 106 | '3DO': { 107 | 'cores': ['opera', '4do'], 108 | 'bios_files': [ 109 | {'file': 'panafz1.bin', 'md5': 'f47264dd47fe30f73ab3c010015c155b', 'required': True}, 110 | {'file': 'panafz10.bin', 'md5': '51f2f43ae2f3508a14d9f56597e2d3ce', 'required': False}, 111 | {'file': 'goldstar.bin', 'md5': '8639fd5e549bd6238cfee79e3e749114', 'required': False}, 112 | ] 113 | }, 114 | 'Microsoft - MSX': { 115 | 'cores': ['bluemsx', 'fmsx'], 116 | 'bios_files': [ 117 | {'file': 'MSX.ROM', 'required': True}, 118 | {'file': 'MSX2.ROM', 'required': False}, 119 | {'file': 'MSX2EXT.ROM', 'required': False}, 120 | {'file': 'MSX2P.ROM', 'required': False}, 121 | {'file': 'MSX2PEXT.ROM', 'required': False}, 122 | ] 123 | }, 124 | 'Commodore - Amiga': { 125 | 'cores': ['puae', 'fsuae'], 126 | 'bios_files': [ 127 | {'file': 'kick33180.A500', 'md5': '85ad74194e87c08904327de1a9443b7a', 'required': True}, 128 | {'file': 'kick34005.A500', 'md5': '82a21c1890cae844b3df741f2762d48d', 'required': True}, 129 | {'file': 'kick37175.A500', 'md5': 'dc10d7bdd1b6f450773dfb558477c230', 'required': False}, 130 | {'file': 'kick40063.A600', 'md5': 'e40a5dfb3d017ba8779faba30cbd1c8e', 'required': False}, 131 | ] 132 | }, 133 | 'Sony - PlayStation Portable': { 134 | 'cores': ['ppsspp'], 135 | 'bios_files': [ 136 | # PPSSPP generates its own PSP firmware files 137 | {'file': 'PPSSPP', 'required': False, 'desc': 'PPSSPP handles firmware internally'}, 138 | ] 139 | }, 140 | 'Nintendo - Nintendo 3DS': { 141 | 'cores': ['citra'], 142 | 'bios_files': [ 143 | {'file': 'boot9.bin', 'required': True}, 144 | {'file': 'boot11.bin', 'required': True}, 145 | {'file': 'sysdata', 'required': True, 'desc': 'System save data folder'}, 146 | ] 147 | } 148 | } 149 | 150 | class BiosManager: 151 | """Manages BIOS files for RetroArch cores""" 152 | 153 | def __init__(self, retroarch_interface, romm_client=None, log_callback=None, settings=None): 154 | self.retroarch = retroarch_interface 155 | self.romm_client = romm_client 156 | self.log = log_callback or print 157 | self.settings = settings # Use passed settings instead of creating new one 158 | 159 | # Find system directory 160 | self.system_dir = self.find_system_directory() 161 | 162 | # Cache for installed BIOS files 163 | self.installed_bios = {} 164 | self.scan_installed_bios() 165 | 166 | # Platform name normalization map 167 | self.platform_aliases = { 168 | 'playstation': 'Sony - PlayStation', 169 | 'ps1': 'Sony - PlayStation', 170 | 'psx': 'Sony - PlayStation', 171 | 'playstation-2': 'Sony - PlayStation 2', 172 | 'ps2': 'Sony - PlayStation 2', 173 | 'sega-saturn': 'Sega - Saturn', 174 | 'saturn': 'Sega - Saturn', 175 | 'sega-cd': 'Sega - Mega-CD - Sega CD', 176 | 'mega-cd': 'Sega - Mega-CD - Sega CD', 177 | 'segacd': 'Sega - Mega-CD - Sega CD', 178 | 'dreamcast': 'Sega - Dreamcast', 179 | 'dc': 'Sega - Dreamcast', 180 | 'neo-geo': 'SNK - Neo Geo', 181 | 'neogeo': 'SNK - Neo Geo', 182 | 'nintendo-ds': 'Nintendo - Nintendo DS', 183 | 'nds': 'Nintendo - Nintendo DS', 184 | 'game-boy-advance': 'Nintendo - Game Boy Advance', 185 | 'gba': 'Nintendo - Game Boy Advance', 186 | 'pc-engine': 'NEC - PC Engine - TurboGrafx 16', 187 | 'turbografx': 'NEC - PC Engine - TurboGrafx 16', 188 | 'turbografx-16': 'NEC - PC Engine - TurboGrafx 16', 189 | 'pce': 'NEC - PC Engine - TurboGrafx 16', 190 | 'atari-7800': 'Atari - 7800', 191 | 'atari-lynx': 'Atari - Lynx', 192 | 'lynx': 'Atari - Lynx', 193 | '3do': '3DO', 194 | 'msx': 'Microsoft - MSX', 195 | 'msx2': 'Microsoft - MSX', 196 | 'amiga': 'Commodore - Amiga', 197 | 'psp': 'Sony - PlayStation Portable', 198 | 'playstation-portable': 'Sony - PlayStation Portable', 199 | '3ds': 'Nintendo - Nintendo 3DS', 200 | 'nintendo-3ds': 'Nintendo - Nintendo 3DS', 201 | } 202 | 203 | def refresh_system_directory(self): 204 | """Refresh system directory path (useful when settings change)""" 205 | self.system_dir = self.find_system_directory() 206 | if self.system_dir: 207 | self.scan_installed_bios() 208 | self.log(f"📁 BIOS directory refreshed: {self.system_dir}") 209 | else: 210 | self.log("⚠️ No BIOS directory found after refresh") 211 | 212 | def find_system_directory(self): 213 | """Find RetroArch system/BIOS directory""" 214 | # Check for custom BIOS path override first 215 | if self.settings: 216 | custom_bios_path = self.settings.get('BIOS', 'custom_path', '').strip() 217 | if custom_bios_path: # Only use if not empty 218 | custom_dir = Path(custom_bios_path) 219 | if custom_dir.exists(): 220 | self.log(f"📁 Using custom BIOS directory: {custom_dir}") 221 | return custom_dir 222 | else: 223 | # Try to create it 224 | try: 225 | custom_dir.mkdir(parents=True, exist_ok=True) 226 | self.log(f"📁 Created custom BIOS directory: {custom_dir}") 227 | return custom_dir 228 | except Exception as e: 229 | self.log(f"❌ Failed to create custom BIOS directory: {e}") 230 | self.log("⚠️ Falling back to auto-detection") 231 | 232 | possible_dirs = [ 233 | # RetroDECK 234 | Path.home() / 'retrodeck' / 'bios', 235 | Path.home() / '.var/app/net.retrodeck.retrodeck/config/retroarch/system', 236 | 237 | # Flatpak RetroArch 238 | Path.home() / '.var/app/org.libretro.RetroArch/config/retroarch/system', 239 | 240 | # Native installations 241 | Path.home() / '.config/retroarch/system', 242 | Path.home() / '.retroarch/system', 243 | 244 | # Steam installations 245 | Path.home() / '.steam/steam/steamapps/common/RetroArch/system', 246 | Path.home() / '.local/share/Steam/steamapps/common/RetroArch/system', 247 | Path.home() / '.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/RetroArch/system', 248 | 249 | # Snap 250 | Path.home() / 'snap/retroarch/current/.config/retroarch/system', 251 | 252 | # AppImage 253 | Path.home() / '.retroarch-appimage/system', 254 | ] 255 | 256 | # Check for custom RetroArch path override 257 | if hasattr(self.retroarch, 'settings'): 258 | custom_path = self.retroarch.settings.get('RetroArch', 'custom_path', '').strip() 259 | if custom_path: 260 | custom_dir = Path(custom_path).parent 261 | possible_dirs.insert(0, custom_dir / 'system') 262 | possible_dirs.insert(0, custom_dir / 'bios') # RetroDECK style 263 | 264 | for system_dir in possible_dirs: 265 | if system_dir.exists(): 266 | self.log(f"📁 Found system/BIOS directory: {system_dir}") 267 | return system_dir 268 | 269 | # Create RetroDECK bios directory if RetroDECK is detected 270 | retrodeck_bios = Path.home() / 'retrodeck' / 'bios' 271 | if Path.home() / 'retrodeck' / 'roms' and not retrodeck_bios.exists(): 272 | try: 273 | retrodeck_bios.mkdir(parents=True, exist_ok=True) 274 | self.log(f"📁 Created RetroDECK BIOS directory: {retrodeck_bios}") 275 | return retrodeck_bios 276 | except Exception as e: 277 | self.log(f"Failed to create RetroDECK BIOS directory: {e}") 278 | 279 | self.log("⚠️ No RetroArch system/BIOS directory found") 280 | return None 281 | 282 | def calculate_md5(self, file_path): 283 | """Calculate MD5 hash of a file""" 284 | md5 = hashlib.md5() 285 | try: 286 | with open(file_path, 'rb') as f: 287 | for chunk in iter(lambda: f.read(8192), b''): 288 | md5.update(chunk) 289 | return md5.hexdigest() 290 | except Exception as e: 291 | self.log(f"Error calculating MD5 for {file_path}: {e}") 292 | return None 293 | 294 | def scan_installed_bios(self): 295 | """Scan for installed BIOS files""" 296 | self.installed_bios = {} 297 | 298 | if not self.system_dir: 299 | return 300 | 301 | try: 302 | # Scan all files in system directory 303 | for file_path in self.system_dir.rglob('*'): 304 | if file_path.is_file(): 305 | # Skip very large files (likely not BIOS) 306 | if file_path.stat().st_size > 50 * 1024 * 1024: # 50MB 307 | continue 308 | 309 | relative_path = file_path.relative_to(self.system_dir) 310 | self.installed_bios[str(relative_path)] = { 311 | 'path': str(file_path), 312 | 'size': file_path.stat().st_size, 313 | 'modified': file_path.stat().st_mtime, 314 | 'md5': None # Calculate on demand to speed up scanning 315 | } 316 | 317 | self.log(f"📋 Found {len(self.installed_bios)} files in system directory") 318 | 319 | except Exception as e: 320 | self.log(f"Error scanning BIOS directory: {e}") 321 | 322 | def normalize_platform_name(self, platform_name): 323 | """Normalize platform name to match BIOS database""" 324 | if not platform_name: 325 | return None 326 | 327 | # Convert to lowercase for comparison 328 | platform_lower = platform_name.lower().replace('_', '-') 329 | 330 | # Check aliases 331 | if platform_lower in self.platform_aliases: 332 | return self.platform_aliases[platform_lower] 333 | 334 | # Check if it's already a proper name 335 | for db_platform in BIOS_DATABASE.keys(): 336 | if db_platform.lower() == platform_lower: 337 | return db_platform 338 | 339 | # Partial matching for complex names 340 | for db_platform in BIOS_DATABASE.keys(): 341 | if platform_lower in db_platform.lower() or db_platform.lower() in platform_lower: 342 | return db_platform 343 | 344 | return platform_name # Return original if no match found 345 | 346 | def check_platform_bios(self, platform_name): 347 | """Check BIOS status for a specific platform""" 348 | platform_name = self.normalize_platform_name(platform_name) 349 | 350 | if platform_name not in BIOS_DATABASE: 351 | return [], [] # No BIOS requirements known 352 | 353 | platform_bios = BIOS_DATABASE[platform_name] 354 | present = [] 355 | missing = [] 356 | 357 | for bios_info in platform_bios['bios_files']: 358 | bios_file = bios_info['file'] 359 | 360 | # Check if file exists 361 | if bios_file in self.installed_bios: 362 | installed = self.installed_bios[bios_file] 363 | 364 | # Verify MD5 if specified 365 | if 'md5' in bios_info: 366 | # Calculate MD5 if not already done 367 | if installed['md5'] is None: 368 | installed['md5'] = self.calculate_md5(Path(installed['path'])) 369 | 370 | if installed['md5'] == bios_info['md5']: 371 | present.append({**bios_info, 'status': 'verified'}) 372 | else: 373 | missing.append({**bios_info, 'status': 'md5_mismatch', 374 | 'actual_md5': installed['md5']}) 375 | 376 | # Verify size if specified 377 | elif 'size' in bios_info: 378 | if installed['size'] == bios_info['size']: 379 | present.append({**bios_info, 'status': 'size_ok'}) 380 | else: 381 | missing.append({**bios_info, 'status': 'size_mismatch', 382 | 'actual_size': installed['size']}) 383 | else: 384 | # No verification criteria, assume OK 385 | present.append({**bios_info, 'status': 'present'}) 386 | 387 | elif not bios_info.get('optional', False): 388 | # Required file is missing 389 | missing.append({**bios_info, 'status': 'missing'}) 390 | 391 | return present, missing 392 | 393 | def get_all_platforms_status(self): 394 | """Get BIOS status for all platforms""" 395 | status = {} 396 | 397 | for platform_name in BIOS_DATABASE.keys(): 398 | present, missing = self.check_platform_bios(platform_name) 399 | 400 | # Only include platforms that need BIOS files 401 | if present or missing: 402 | status[platform_name] = { 403 | 'present': present, 404 | 'missing': missing, 405 | 'complete': len(missing) == 0, 406 | 'required_count': len([b for b in missing if not b.get('optional', False)]) 407 | } 408 | 409 | return status 410 | 411 | def download_bios_from_romm(self, platform_name, bios_filename): 412 | """Download a specific BIOS file from RomM's firmware API""" 413 | if not self.romm_client or not self.romm_client.authenticated: 414 | self.log("❌ Not connected to RomM") 415 | return False 416 | 417 | if not self.system_dir: 418 | self.log("❌ No system directory found") 419 | return False 420 | 421 | try: 422 | from urllib.parse import urljoin 423 | 424 | # This part of your code is correct 425 | platforms_response = self.romm_client.session.get( 426 | urljoin(self.romm_client.base_url, '/api/platforms'), 427 | timeout=10 428 | ) 429 | 430 | if platforms_response.status_code != 200: 431 | self.log("❌ Failed to get platforms list from RomM.") 432 | return False 433 | 434 | platforms = platforms_response.json() 435 | 436 | platform_mappings = { 437 | 'Sony - PlayStation': ['PlayStation', 'Sony PlayStation', 'PS1', 'PSX'], 438 | 'Nintendo - Game Boy Advance': ['Game Boy Advance', 'GBA', 'Nintendo Game Boy Advance'], 439 | 'Nintendo - Game Boy': ['Game Boy', 'GB', 'Nintendo Game Boy'], 440 | 'Nintendo - Game Boy Color': ['Game Boy Color', 'GBC', 'Nintendo Game Boy Color'] 441 | } 442 | 443 | possible_names = platform_mappings.get(platform_name, [platform_name]) 444 | 445 | for platform in platforms: 446 | platform_name_check = platform.get('name', '') 447 | 448 | if any(name.lower() in platform_name_check.lower() or 449 | platform_name_check.lower() in name.lower() 450 | for name in possible_names): 451 | 452 | self.log(f"🔍 Found platform: {platform_name_check}") 453 | firmware_list = platform.get('firmware', []) 454 | 455 | for firmware in firmware_list: 456 | if firmware.get('file_name') == bios_filename: 457 | firmware_id = firmware.get('id') 458 | self.log(f"🔍 Found BIOS: {bios_filename} (ID: {firmware_id})") 459 | 460 | # Construct the download URL using the firmware ID and filename 461 | download_url = f'/api/firmware/{firmware_id}/content/{bios_filename}' 462 | 463 | # STEP 1: Download the file from the constructed URL 464 | file_response = self.romm_client.session.get( 465 | urljoin(self.romm_client.base_url, download_url), 466 | stream=True, 467 | timeout=60 # Increased timeout for larger files 468 | ) 469 | 470 | # STEP 2: Check for a successful response and write the file 471 | if file_response.status_code == 200: 472 | download_path = self.system_dir / bios_filename 473 | 474 | with open(download_path, 'wb') as f: 475 | for chunk in file_response.iter_content(chunk_size=8192): 476 | f.write(chunk) 477 | 478 | self.log(f"✅ Downloaded {bios_filename}") 479 | return True 480 | else: 481 | self.log(f"❌ Download failed with status code: {file_response.status_code}") 482 | return False 483 | 484 | self.log(f"❌ {bios_filename} not found in {platform_name_check} firmware list on server.") 485 | break # Stop searching after finding the correct platform 486 | 487 | self.log(f"❌ Platform matching '{platform_name}' not found on server.") 488 | return False 489 | 490 | except Exception as e: 491 | self.log(f"❌ Download error: {e}") 492 | import traceback 493 | self.log(traceback.format_exc()) # More detailed error for debugging 494 | return False 495 | 496 | def search_romm_for_bios(self, bios_filename): 497 | """Search RomM for a BIOS file""" 498 | if not self.romm_client: 499 | return None 500 | 501 | try: 502 | # Try searching via API 503 | search_endpoints = [ 504 | f'/api/search?q={bios_filename}', 505 | f'/api/search?q={bios_filename}&type=resource', 506 | f'/api/search?q={bios_filename}&type=firmware', 507 | f'/api/resources?search={bios_filename}', 508 | ] 509 | 510 | for endpoint in search_endpoints: 511 | try: 512 | response = self.romm_client.session.get( 513 | urljoin(self.romm_client.base_url, endpoint), 514 | timeout=10 515 | ) 516 | 517 | if response.status_code == 200: 518 | results = response.json() 519 | 520 | if isinstance(results, list): 521 | for result in results: 522 | if isinstance(result, dict): 523 | filename = result.get('filename', result.get('name', '')) 524 | if filename.lower() == bios_filename.lower(): 525 | return result 526 | elif isinstance(results, dict): 527 | items = results.get('items', results.get('results', [])) 528 | for item in items: 529 | filename = item.get('filename', item.get('name', '')) 530 | if filename.lower() == bios_filename.lower(): 531 | return item 532 | 533 | except: 534 | continue 535 | 536 | except Exception as e: 537 | self.log(f"Search error: {e}") 538 | 539 | return None 540 | 541 | def download_romm_resource(self, resource_info): 542 | """Download a resource from RomM based on search result""" 543 | if not self.romm_client or not resource_info: 544 | return False 545 | 546 | try: 547 | # Extract download URL from resource info 548 | download_url = None 549 | 550 | if 'download_url' in resource_info: 551 | download_url = resource_info['download_url'] 552 | elif 'url' in resource_info: 553 | download_url = resource_info['url'] 554 | elif 'path' in resource_info: 555 | download_url = f"/api/resources/{resource_info['id']}/download" 556 | elif 'id' in resource_info: 557 | download_url = f"/api/resources/{resource_info['id']}/content" 558 | 559 | if download_url: 560 | response = self.romm_client.session.get( 561 | urljoin(self.romm_client.base_url, download_url), 562 | stream=True, 563 | timeout=30 564 | ) 565 | 566 | if response.status_code == 200: 567 | filename = resource_info.get('filename', resource_info.get('name', 'unknown.bin')) 568 | download_path = self.system_dir / filename 569 | 570 | with open(download_path, 'wb') as f: 571 | for chunk in response.iter_content(chunk_size=8192): 572 | if chunk: 573 | f.write(chunk) 574 | 575 | self.log(f"✅ Downloaded {filename}") 576 | return True 577 | 578 | except Exception as e: 579 | self.log(f"Resource download error: {e}") 580 | 581 | return False 582 | 583 | def auto_download_missing_bios(self, platform_name): 584 | """Download missing BIOS for a specific platform""" 585 | # Rescan to get current state 586 | self.scan_installed_bios() 587 | 588 | present, missing = self.check_platform_bios(platform_name) 589 | 590 | # Only download required files that are missing 591 | required_missing = [b for b in missing if not b.get('optional', False)] 592 | 593 | if not required_missing: 594 | self.log(f"✅ All required BIOS present for {platform_name}") 595 | return True 596 | 597 | success_count = 0 598 | for bios_info in required_missing: 599 | bios_file = bios_info['file'] 600 | 601 | # Double-check if file exists before downloading 602 | bios_path = self.system_dir / bios_file 603 | if bios_path.exists(): 604 | self.log(f"⏭️ {bios_file} already exists, skipping") 605 | success_count += 1 606 | continue 607 | 608 | if self.download_bios_from_romm(platform_name, bios_file): 609 | success_count += 1 610 | 611 | # Rescan after downloads 612 | self.scan_installed_bios() 613 | 614 | if success_count == len(required_missing): 615 | self.log(f"✅ Downloaded all {success_count} BIOS files for {platform_name}") 616 | return True 617 | elif success_count > 0: 618 | self.log(f"⚠️ Downloaded {success_count}/{len(required_missing)} BIOS files for {platform_name}") 619 | return True 620 | else: 621 | self.log(f"❌ Could not download any BIOS files for {platform_name}") 622 | return False -------------------------------------------------------------------------------- /decky_plugin/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@decky/api': 12 | specifier: ^1.1.2 13 | version: 1.1.2 14 | react-icons: 15 | specifier: ^5.3.0 16 | version: 5.3.0(react@18.3.1) 17 | tslib: 18 | specifier: ^2.7.0 19 | version: 2.7.0 20 | devDependencies: 21 | '@decky/rollup': 22 | specifier: ^1.0.1 23 | version: 1.0.1 24 | '@decky/ui': 25 | specifier: ^4.7.2 26 | version: 4.7.2 27 | '@types/react': 28 | specifier: 18.3.3 29 | version: 18.3.3 30 | '@types/react-dom': 31 | specifier: 18.3.0 32 | version: 18.3.0 33 | '@types/webpack': 34 | specifier: ^5.28.5 35 | version: 5.28.5 36 | rollup: 37 | specifier: ^4.22.5 38 | version: 4.22.5 39 | typescript: 40 | specifier: ^5.6.2 41 | version: 5.6.2 42 | 43 | packages: 44 | 45 | '@decky/api@1.1.2': 46 | resolution: {integrity: sha512-lTMqRpHOrGTCyH2c1jJvkmWhOq2dcnX5/ioHbfCVmyQOBik1OM1BnzF1uROsnNDC5GkRvl3J/ATqYp6vhYpRqw==} 47 | 48 | '@decky/rollup@1.0.1': 49 | resolution: {integrity: sha512-dx1VJwD7ul14PA/aZvOwAfY/GujHzqZJ+MFb4OIUVi63/z4KWMSuZrK6QWo0S4LrNW3RzB3ua6LT0WcJaNY9gw==} 50 | 51 | '@decky/ui@4.7.2': 52 | resolution: {integrity: sha512-jYXVhbyyupXAcCuFqr7G2qjYVjp8hlMGF8zl8ALv67y0YhikAtfhA2rGUjCuaV3kdo9YrpBh8djRUJXdFPg/Eg==} 53 | 54 | '@isaacs/cliui@8.0.2': 55 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 56 | engines: {node: '>=12'} 57 | 58 | '@jridgewell/gen-mapping@0.3.5': 59 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 60 | engines: {node: '>=6.0.0'} 61 | 62 | '@jridgewell/resolve-uri@3.1.2': 63 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 64 | engines: {node: '>=6.0.0'} 65 | 66 | '@jridgewell/set-array@1.2.1': 67 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 68 | engines: {node: '>=6.0.0'} 69 | 70 | '@jridgewell/source-map@0.3.6': 71 | resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} 72 | 73 | '@jridgewell/sourcemap-codec@1.5.0': 74 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 75 | 76 | '@jridgewell/trace-mapping@0.3.25': 77 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 78 | 79 | '@nodelib/fs.scandir@2.1.5': 80 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 81 | engines: {node: '>= 8'} 82 | 83 | '@nodelib/fs.stat@2.0.5': 84 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 85 | engines: {node: '>= 8'} 86 | 87 | '@nodelib/fs.walk@1.2.8': 88 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 89 | engines: {node: '>= 8'} 90 | 91 | '@pkgjs/parseargs@0.11.0': 92 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 93 | engines: {node: '>=14'} 94 | 95 | '@rollup/plugin-commonjs@26.0.3': 96 | resolution: {integrity: sha512-2BJcolt43MY+y5Tz47djHkodCC3c1VKVrBDKpVqHKpQ9z9S158kCCqB8NF6/gzxLdNlYW9abB3Ibh+kOWLp8KQ==} 97 | engines: {node: '>=16.0.0 || 14 >= 14.17'} 98 | peerDependencies: 99 | rollup: ^2.68.0||^3.0.0||^4.0.0 100 | peerDependenciesMeta: 101 | rollup: 102 | optional: true 103 | 104 | '@rollup/plugin-json@6.1.0': 105 | resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} 106 | engines: {node: '>=14.0.0'} 107 | peerDependencies: 108 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 109 | peerDependenciesMeta: 110 | rollup: 111 | optional: true 112 | 113 | '@rollup/plugin-node-resolve@15.3.0': 114 | resolution: {integrity: sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==} 115 | engines: {node: '>=14.0.0'} 116 | peerDependencies: 117 | rollup: ^2.78.0||^3.0.0||^4.0.0 118 | peerDependenciesMeta: 119 | rollup: 120 | optional: true 121 | 122 | '@rollup/plugin-replace@5.0.7': 123 | resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} 124 | engines: {node: '>=14.0.0'} 125 | peerDependencies: 126 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 127 | peerDependenciesMeta: 128 | rollup: 129 | optional: true 130 | 131 | '@rollup/plugin-typescript@11.1.6': 132 | resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} 133 | engines: {node: '>=14.0.0'} 134 | peerDependencies: 135 | rollup: ^2.14.0||^3.0.0||^4.0.0 136 | tslib: '*' 137 | typescript: '>=3.7.0' 138 | peerDependenciesMeta: 139 | rollup: 140 | optional: true 141 | tslib: 142 | optional: true 143 | 144 | '@rollup/pluginutils@5.1.2': 145 | resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==} 146 | engines: {node: '>=14.0.0'} 147 | peerDependencies: 148 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 149 | peerDependenciesMeta: 150 | rollup: 151 | optional: true 152 | 153 | '@rollup/rollup-android-arm-eabi@4.22.5': 154 | resolution: {integrity: sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==} 155 | cpu: [arm] 156 | os: [android] 157 | 158 | '@rollup/rollup-android-arm64@4.22.5': 159 | resolution: {integrity: sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==} 160 | cpu: [arm64] 161 | os: [android] 162 | 163 | '@rollup/rollup-darwin-arm64@4.22.5': 164 | resolution: {integrity: sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==} 165 | cpu: [arm64] 166 | os: [darwin] 167 | 168 | '@rollup/rollup-darwin-x64@4.22.5': 169 | resolution: {integrity: sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==} 170 | cpu: [x64] 171 | os: [darwin] 172 | 173 | '@rollup/rollup-linux-arm-gnueabihf@4.22.5': 174 | resolution: {integrity: sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==} 175 | cpu: [arm] 176 | os: [linux] 177 | 178 | '@rollup/rollup-linux-arm-musleabihf@4.22.5': 179 | resolution: {integrity: sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==} 180 | cpu: [arm] 181 | os: [linux] 182 | 183 | '@rollup/rollup-linux-arm64-gnu@4.22.5': 184 | resolution: {integrity: sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==} 185 | cpu: [arm64] 186 | os: [linux] 187 | 188 | '@rollup/rollup-linux-arm64-musl@4.22.5': 189 | resolution: {integrity: sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==} 190 | cpu: [arm64] 191 | os: [linux] 192 | 193 | '@rollup/rollup-linux-powerpc64le-gnu@4.22.5': 194 | resolution: {integrity: sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==} 195 | cpu: [ppc64] 196 | os: [linux] 197 | 198 | '@rollup/rollup-linux-riscv64-gnu@4.22.5': 199 | resolution: {integrity: sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==} 200 | cpu: [riscv64] 201 | os: [linux] 202 | 203 | '@rollup/rollup-linux-s390x-gnu@4.22.5': 204 | resolution: {integrity: sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==} 205 | cpu: [s390x] 206 | os: [linux] 207 | 208 | '@rollup/rollup-linux-x64-gnu@4.22.5': 209 | resolution: {integrity: sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==} 210 | cpu: [x64] 211 | os: [linux] 212 | 213 | '@rollup/rollup-linux-x64-musl@4.22.5': 214 | resolution: {integrity: sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==} 215 | cpu: [x64] 216 | os: [linux] 217 | 218 | '@rollup/rollup-win32-arm64-msvc@4.22.5': 219 | resolution: {integrity: sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==} 220 | cpu: [arm64] 221 | os: [win32] 222 | 223 | '@rollup/rollup-win32-ia32-msvc@4.22.5': 224 | resolution: {integrity: sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==} 225 | cpu: [ia32] 226 | os: [win32] 227 | 228 | '@rollup/rollup-win32-x64-msvc@4.22.5': 229 | resolution: {integrity: sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==} 230 | cpu: [x64] 231 | os: [win32] 232 | 233 | '@types/estree@1.0.6': 234 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 235 | 236 | '@types/glob@7.2.0': 237 | resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} 238 | 239 | '@types/json-schema@7.0.15': 240 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 241 | 242 | '@types/minimatch@5.1.2': 243 | resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} 244 | 245 | '@types/node@22.7.4': 246 | resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} 247 | 248 | '@types/prop-types@15.7.13': 249 | resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} 250 | 251 | '@types/react-dom@18.3.0': 252 | resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} 253 | 254 | '@types/react@18.3.3': 255 | resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} 256 | 257 | '@types/resolve@1.20.2': 258 | resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} 259 | 260 | '@types/webpack@5.28.5': 261 | resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} 262 | 263 | '@webassemblyjs/ast@1.12.1': 264 | resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} 265 | 266 | '@webassemblyjs/floating-point-hex-parser@1.11.6': 267 | resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} 268 | 269 | '@webassemblyjs/helper-api-error@1.11.6': 270 | resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} 271 | 272 | '@webassemblyjs/helper-buffer@1.12.1': 273 | resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} 274 | 275 | '@webassemblyjs/helper-numbers@1.11.6': 276 | resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} 277 | 278 | '@webassemblyjs/helper-wasm-bytecode@1.11.6': 279 | resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} 280 | 281 | '@webassemblyjs/helper-wasm-section@1.12.1': 282 | resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} 283 | 284 | '@webassemblyjs/ieee754@1.11.6': 285 | resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} 286 | 287 | '@webassemblyjs/leb128@1.11.6': 288 | resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} 289 | 290 | '@webassemblyjs/utf8@1.11.6': 291 | resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} 292 | 293 | '@webassemblyjs/wasm-edit@1.12.1': 294 | resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} 295 | 296 | '@webassemblyjs/wasm-gen@1.12.1': 297 | resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} 298 | 299 | '@webassemblyjs/wasm-opt@1.12.1': 300 | resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} 301 | 302 | '@webassemblyjs/wasm-parser@1.12.1': 303 | resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} 304 | 305 | '@webassemblyjs/wast-printer@1.12.1': 306 | resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} 307 | 308 | '@xtuc/ieee754@1.2.0': 309 | resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} 310 | 311 | '@xtuc/long@4.2.2': 312 | resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 313 | 314 | acorn-import-attributes@1.9.5: 315 | resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} 316 | peerDependencies: 317 | acorn: ^8 318 | 319 | acorn@8.12.1: 320 | resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} 321 | engines: {node: '>=0.4.0'} 322 | hasBin: true 323 | 324 | aggregate-error@3.1.0: 325 | resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} 326 | engines: {node: '>=8'} 327 | 328 | ajv-keywords@3.5.2: 329 | resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} 330 | peerDependencies: 331 | ajv: ^6.9.1 332 | 333 | ajv@6.12.6: 334 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 335 | 336 | ansi-regex@5.0.1: 337 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 338 | engines: {node: '>=8'} 339 | 340 | ansi-regex@6.1.0: 341 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 342 | engines: {node: '>=12'} 343 | 344 | ansi-styles@4.3.0: 345 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 346 | engines: {node: '>=8'} 347 | 348 | ansi-styles@6.2.1: 349 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 350 | engines: {node: '>=12'} 351 | 352 | array-union@2.1.0: 353 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 354 | engines: {node: '>=8'} 355 | 356 | balanced-match@1.0.2: 357 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 358 | 359 | brace-expansion@1.1.11: 360 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 361 | 362 | brace-expansion@2.0.1: 363 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 364 | 365 | braces@3.0.3: 366 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 367 | engines: {node: '>=8'} 368 | 369 | browserslist@4.24.0: 370 | resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} 371 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 372 | hasBin: true 373 | 374 | buffer-from@1.1.2: 375 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 376 | 377 | caniuse-lite@1.0.30001664: 378 | resolution: {integrity: sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==} 379 | 380 | chrome-trace-event@1.0.4: 381 | resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} 382 | engines: {node: '>=6.0'} 383 | 384 | clean-stack@2.2.0: 385 | resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} 386 | engines: {node: '>=6'} 387 | 388 | color-convert@2.0.1: 389 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 390 | engines: {node: '>=7.0.0'} 391 | 392 | color-name@1.1.4: 393 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 394 | 395 | commander@2.20.3: 396 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 397 | 398 | commondir@1.0.1: 399 | resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} 400 | 401 | concat-map@0.0.1: 402 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 403 | 404 | cross-spawn@7.0.3: 405 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 406 | engines: {node: '>= 8'} 407 | 408 | csstype@3.1.3: 409 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 410 | 411 | deepmerge@4.3.1: 412 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 413 | engines: {node: '>=0.10.0'} 414 | 415 | del@5.1.0: 416 | resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} 417 | engines: {node: '>=8'} 418 | 419 | dir-glob@3.0.1: 420 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 421 | engines: {node: '>=8'} 422 | 423 | eastasianwidth@0.2.0: 424 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 425 | 426 | electron-to-chromium@1.5.29: 427 | resolution: {integrity: sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==} 428 | 429 | emoji-regex@8.0.0: 430 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 431 | 432 | emoji-regex@9.2.2: 433 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 434 | 435 | enhanced-resolve@5.17.1: 436 | resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} 437 | engines: {node: '>=10.13.0'} 438 | 439 | es-module-lexer@1.5.4: 440 | resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} 441 | 442 | escalade@3.2.0: 443 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 444 | engines: {node: '>=6'} 445 | 446 | eslint-scope@5.1.1: 447 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 448 | engines: {node: '>=8.0.0'} 449 | 450 | esrecurse@4.3.0: 451 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 452 | engines: {node: '>=4.0'} 453 | 454 | estraverse@4.3.0: 455 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 456 | engines: {node: '>=4.0'} 457 | 458 | estraverse@5.3.0: 459 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 460 | engines: {node: '>=4.0'} 461 | 462 | estree-walker@0.6.1: 463 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 464 | 465 | estree-walker@2.0.2: 466 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 467 | 468 | estree-walker@3.0.3: 469 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 470 | 471 | events@3.3.0: 472 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 473 | engines: {node: '>=0.8.x'} 474 | 475 | fast-deep-equal@3.1.3: 476 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 477 | 478 | fast-glob@3.3.2: 479 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 480 | engines: {node: '>=8.6.0'} 481 | 482 | fast-json-stable-stringify@2.1.0: 483 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 484 | 485 | fastq@1.17.1: 486 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 487 | 488 | fill-range@7.1.1: 489 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 490 | engines: {node: '>=8'} 491 | 492 | foreground-child@3.3.0: 493 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 494 | engines: {node: '>=14'} 495 | 496 | fs.realpath@1.0.0: 497 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 498 | 499 | fsevents@2.3.3: 500 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 501 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 502 | os: [darwin] 503 | 504 | function-bind@1.1.2: 505 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 506 | 507 | glob-parent@5.1.2: 508 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 509 | engines: {node: '>= 6'} 510 | 511 | glob-to-regexp@0.4.1: 512 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 513 | 514 | glob@10.4.5: 515 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 516 | hasBin: true 517 | 518 | glob@7.2.3: 519 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 520 | deprecated: Glob versions prior to v9 are no longer supported 521 | 522 | globby@10.0.2: 523 | resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} 524 | engines: {node: '>=8'} 525 | 526 | graceful-fs@4.2.11: 527 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 528 | 529 | has-flag@4.0.0: 530 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 531 | engines: {node: '>=8'} 532 | 533 | hasown@2.0.2: 534 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 535 | engines: {node: '>= 0.4'} 536 | 537 | ignore@5.3.2: 538 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 539 | engines: {node: '>= 4'} 540 | 541 | indent-string@4.0.0: 542 | resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} 543 | engines: {node: '>=8'} 544 | 545 | inflight@1.0.6: 546 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 547 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 548 | 549 | inherits@2.0.4: 550 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 551 | 552 | is-core-module@2.15.1: 553 | resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} 554 | engines: {node: '>= 0.4'} 555 | 556 | is-extglob@2.1.1: 557 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 558 | engines: {node: '>=0.10.0'} 559 | 560 | is-fullwidth-code-point@3.0.0: 561 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 562 | engines: {node: '>=8'} 563 | 564 | is-glob@4.0.3: 565 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 566 | engines: {node: '>=0.10.0'} 567 | 568 | is-module@1.0.0: 569 | resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 570 | 571 | is-number@7.0.0: 572 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 573 | engines: {node: '>=0.12.0'} 574 | 575 | is-path-cwd@2.2.0: 576 | resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} 577 | engines: {node: '>=6'} 578 | 579 | is-path-inside@3.0.3: 580 | resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} 581 | engines: {node: '>=8'} 582 | 583 | is-reference@1.2.1: 584 | resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} 585 | 586 | is-reference@3.0.2: 587 | resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} 588 | 589 | is-what@5.0.2: 590 | resolution: {integrity: sha512-vI7Ui0qzNQ2ClDZd0bC7uqRk3T1imbX5cZODmVlqqdqiwmSIUX3CNSiRgFjFMJ987sVCMSa7xZeEDtpJduPg4A==} 591 | engines: {node: '>=18'} 592 | 593 | isexe@2.0.0: 594 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 595 | 596 | jackspeak@3.4.3: 597 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 598 | 599 | jest-worker@27.5.1: 600 | resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} 601 | engines: {node: '>= 10.13.0'} 602 | 603 | js-tokens@4.0.0: 604 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 605 | 606 | json-parse-even-better-errors@2.3.1: 607 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 608 | 609 | json-schema-traverse@0.4.1: 610 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 611 | 612 | loader-runner@4.3.0: 613 | resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} 614 | engines: {node: '>=6.11.5'} 615 | 616 | loose-envify@1.4.0: 617 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 618 | hasBin: true 619 | 620 | lru-cache@10.4.3: 621 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 622 | 623 | magic-string@0.30.11: 624 | resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} 625 | 626 | merge-anything@6.0.2: 627 | resolution: {integrity: sha512-U8x6DL/YVudOcf82B6hd8GFg+6gF6hEHYwzqdo67GrH6vnDZ5YBq6BYX3hHWyCnG3CcqJDB1a9tj9fzMI3RL9Q==} 628 | engines: {node: '>=18'} 629 | 630 | merge-stream@2.0.0: 631 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 632 | 633 | merge2@1.4.1: 634 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 635 | engines: {node: '>= 8'} 636 | 637 | micromatch@4.0.8: 638 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 639 | engines: {node: '>=8.6'} 640 | 641 | mime-db@1.52.0: 642 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 643 | engines: {node: '>= 0.6'} 644 | 645 | mime-types@2.1.35: 646 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 647 | engines: {node: '>= 0.6'} 648 | 649 | minimatch@3.1.2: 650 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 651 | 652 | minimatch@9.0.5: 653 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 654 | engines: {node: '>=16 || 14 >=14.17'} 655 | 656 | minipass@7.1.2: 657 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 658 | engines: {node: '>=16 || 14 >=14.17'} 659 | 660 | neo-async@2.6.2: 661 | resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 662 | 663 | node-releases@2.0.18: 664 | resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} 665 | 666 | once@1.4.0: 667 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 668 | 669 | p-map@3.0.0: 670 | resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} 671 | engines: {node: '>=8'} 672 | 673 | package-json-from-dist@1.0.1: 674 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 675 | 676 | path-is-absolute@1.0.1: 677 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 678 | engines: {node: '>=0.10.0'} 679 | 680 | path-key@3.1.1: 681 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 682 | engines: {node: '>=8'} 683 | 684 | path-parse@1.0.7: 685 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 686 | 687 | path-scurry@1.11.1: 688 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 689 | engines: {node: '>=16 || 14 >=14.18'} 690 | 691 | path-type@4.0.0: 692 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 693 | engines: {node: '>=8'} 694 | 695 | picocolors@1.1.0: 696 | resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} 697 | 698 | picomatch@2.3.1: 699 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 700 | engines: {node: '>=8.6'} 701 | 702 | punycode@2.3.1: 703 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 704 | engines: {node: '>=6'} 705 | 706 | queue-microtask@1.2.3: 707 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 708 | 709 | randombytes@2.1.0: 710 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 711 | 712 | react-icons@5.3.0: 713 | resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} 714 | peerDependencies: 715 | react: '*' 716 | 717 | react@18.3.1: 718 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 719 | engines: {node: '>=0.10.0'} 720 | 721 | resolve@1.22.8: 722 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 723 | hasBin: true 724 | 725 | reusify@1.0.4: 726 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 727 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 728 | 729 | rimraf@3.0.2: 730 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 731 | deprecated: Rimraf versions prior to v4 are no longer supported 732 | hasBin: true 733 | 734 | rollup-plugin-delete@2.1.0: 735 | resolution: {integrity: sha512-TEbqJd7giLvzQDTu4jSPufwhTJs/iYVN2LfR/YIYkqjC/oZ0/h9Q0AeljifIhzBzJYZtHQTWKEbMms5fbh54pw==} 736 | engines: {node: '>=10'} 737 | peerDependencies: 738 | rollup: '*' 739 | 740 | rollup-plugin-external-globals@0.11.0: 741 | resolution: {integrity: sha512-LR+sH2WkgWMPxsA5o5rT7uW7BeWXSeygLe60QQi9qoN/ufaCuHDaVOIbndIkqDPnZt/wZugJh5DCzkZFdSWlLQ==} 742 | peerDependencies: 743 | rollup: ^2.25.0 || ^3.3.0 || ^4.1.4 744 | 745 | rollup-plugin-import-assets@1.1.1: 746 | resolution: {integrity: sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g==} 747 | peerDependencies: 748 | rollup: '>=1.9.0' 749 | 750 | rollup-pluginutils@2.8.2: 751 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 752 | 753 | rollup@4.22.5: 754 | resolution: {integrity: sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==} 755 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 756 | hasBin: true 757 | 758 | run-parallel@1.2.0: 759 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 760 | 761 | safe-buffer@5.2.1: 762 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 763 | 764 | schema-utils@3.3.0: 765 | resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} 766 | engines: {node: '>= 10.13.0'} 767 | 768 | serialize-javascript@6.0.2: 769 | resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} 770 | 771 | shebang-command@2.0.0: 772 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 773 | engines: {node: '>=8'} 774 | 775 | shebang-regex@3.0.0: 776 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 777 | engines: {node: '>=8'} 778 | 779 | signal-exit@4.1.0: 780 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 781 | engines: {node: '>=14'} 782 | 783 | slash@3.0.0: 784 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 785 | engines: {node: '>=8'} 786 | 787 | source-map-support@0.5.21: 788 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 789 | 790 | source-map@0.6.1: 791 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 792 | engines: {node: '>=0.10.0'} 793 | 794 | string-width@4.2.3: 795 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 796 | engines: {node: '>=8'} 797 | 798 | string-width@5.1.2: 799 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 800 | engines: {node: '>=12'} 801 | 802 | strip-ansi@6.0.1: 803 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 804 | engines: {node: '>=8'} 805 | 806 | strip-ansi@7.1.0: 807 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 808 | engines: {node: '>=12'} 809 | 810 | supports-color@8.1.1: 811 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 812 | engines: {node: '>=10'} 813 | 814 | supports-preserve-symlinks-flag@1.0.0: 815 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 816 | engines: {node: '>= 0.4'} 817 | 818 | tapable@2.2.1: 819 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 820 | engines: {node: '>=6'} 821 | 822 | terser-webpack-plugin@5.3.10: 823 | resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} 824 | engines: {node: '>= 10.13.0'} 825 | peerDependencies: 826 | '@swc/core': '*' 827 | esbuild: '*' 828 | uglify-js: '*' 829 | webpack: ^5.1.0 830 | peerDependenciesMeta: 831 | '@swc/core': 832 | optional: true 833 | esbuild: 834 | optional: true 835 | uglify-js: 836 | optional: true 837 | 838 | terser@5.34.0: 839 | resolution: {integrity: sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==} 840 | engines: {node: '>=10'} 841 | hasBin: true 842 | 843 | to-regex-range@5.0.1: 844 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 845 | engines: {node: '>=8.0'} 846 | 847 | tslib@2.7.0: 848 | resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} 849 | 850 | typescript@5.6.2: 851 | resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} 852 | engines: {node: '>=14.17'} 853 | hasBin: true 854 | 855 | undici-types@6.19.8: 856 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 857 | 858 | update-browserslist-db@1.1.1: 859 | resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} 860 | hasBin: true 861 | peerDependencies: 862 | browserslist: '>= 4.21.0' 863 | 864 | uri-js@4.4.1: 865 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 866 | 867 | url-join@4.0.1: 868 | resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} 869 | 870 | watchpack@2.4.2: 871 | resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} 872 | engines: {node: '>=10.13.0'} 873 | 874 | webpack-sources@3.2.3: 875 | resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} 876 | engines: {node: '>=10.13.0'} 877 | 878 | webpack@5.95.0: 879 | resolution: {integrity: sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==} 880 | engines: {node: '>=10.13.0'} 881 | hasBin: true 882 | peerDependencies: 883 | webpack-cli: '*' 884 | peerDependenciesMeta: 885 | webpack-cli: 886 | optional: true 887 | 888 | which@2.0.2: 889 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 890 | engines: {node: '>= 8'} 891 | hasBin: true 892 | 893 | wrap-ansi@7.0.0: 894 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 895 | engines: {node: '>=10'} 896 | 897 | wrap-ansi@8.1.0: 898 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 899 | engines: {node: '>=12'} 900 | 901 | wrappy@1.0.2: 902 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 903 | 904 | snapshots: 905 | 906 | '@decky/api@1.1.2': {} 907 | 908 | '@decky/rollup@1.0.1': 909 | dependencies: 910 | '@rollup/plugin-commonjs': 26.0.3(rollup@4.22.5) 911 | '@rollup/plugin-json': 6.1.0(rollup@4.22.5) 912 | '@rollup/plugin-node-resolve': 15.3.0(rollup@4.22.5) 913 | '@rollup/plugin-replace': 5.0.7(rollup@4.22.5) 914 | '@rollup/plugin-typescript': 11.1.6(rollup@4.22.5)(tslib@2.7.0)(typescript@5.6.2) 915 | merge-anything: 6.0.2 916 | rollup: 4.22.5 917 | rollup-plugin-delete: 2.1.0(rollup@4.22.5) 918 | rollup-plugin-external-globals: 0.11.0(rollup@4.22.5) 919 | rollup-plugin-import-assets: 1.1.1(rollup@4.22.5) 920 | tslib: 2.7.0 921 | typescript: 5.6.2 922 | 923 | '@decky/ui@4.7.2': {} 924 | 925 | '@isaacs/cliui@8.0.2': 926 | dependencies: 927 | string-width: 5.1.2 928 | string-width-cjs: string-width@4.2.3 929 | strip-ansi: 7.1.0 930 | strip-ansi-cjs: strip-ansi@6.0.1 931 | wrap-ansi: 8.1.0 932 | wrap-ansi-cjs: wrap-ansi@7.0.0 933 | 934 | '@jridgewell/gen-mapping@0.3.5': 935 | dependencies: 936 | '@jridgewell/set-array': 1.2.1 937 | '@jridgewell/sourcemap-codec': 1.5.0 938 | '@jridgewell/trace-mapping': 0.3.25 939 | 940 | '@jridgewell/resolve-uri@3.1.2': {} 941 | 942 | '@jridgewell/set-array@1.2.1': {} 943 | 944 | '@jridgewell/source-map@0.3.6': 945 | dependencies: 946 | '@jridgewell/gen-mapping': 0.3.5 947 | '@jridgewell/trace-mapping': 0.3.25 948 | 949 | '@jridgewell/sourcemap-codec@1.5.0': {} 950 | 951 | '@jridgewell/trace-mapping@0.3.25': 952 | dependencies: 953 | '@jridgewell/resolve-uri': 3.1.2 954 | '@jridgewell/sourcemap-codec': 1.5.0 955 | 956 | '@nodelib/fs.scandir@2.1.5': 957 | dependencies: 958 | '@nodelib/fs.stat': 2.0.5 959 | run-parallel: 1.2.0 960 | 961 | '@nodelib/fs.stat@2.0.5': {} 962 | 963 | '@nodelib/fs.walk@1.2.8': 964 | dependencies: 965 | '@nodelib/fs.scandir': 2.1.5 966 | fastq: 1.17.1 967 | 968 | '@pkgjs/parseargs@0.11.0': 969 | optional: true 970 | 971 | '@rollup/plugin-commonjs@26.0.3(rollup@4.22.5)': 972 | dependencies: 973 | '@rollup/pluginutils': 5.1.2(rollup@4.22.5) 974 | commondir: 1.0.1 975 | estree-walker: 2.0.2 976 | glob: 10.4.5 977 | is-reference: 1.2.1 978 | magic-string: 0.30.11 979 | optionalDependencies: 980 | rollup: 4.22.5 981 | 982 | '@rollup/plugin-json@6.1.0(rollup@4.22.5)': 983 | dependencies: 984 | '@rollup/pluginutils': 5.1.2(rollup@4.22.5) 985 | optionalDependencies: 986 | rollup: 4.22.5 987 | 988 | '@rollup/plugin-node-resolve@15.3.0(rollup@4.22.5)': 989 | dependencies: 990 | '@rollup/pluginutils': 5.1.2(rollup@4.22.5) 991 | '@types/resolve': 1.20.2 992 | deepmerge: 4.3.1 993 | is-module: 1.0.0 994 | resolve: 1.22.8 995 | optionalDependencies: 996 | rollup: 4.22.5 997 | 998 | '@rollup/plugin-replace@5.0.7(rollup@4.22.5)': 999 | dependencies: 1000 | '@rollup/pluginutils': 5.1.2(rollup@4.22.5) 1001 | magic-string: 0.30.11 1002 | optionalDependencies: 1003 | rollup: 4.22.5 1004 | 1005 | '@rollup/plugin-typescript@11.1.6(rollup@4.22.5)(tslib@2.7.0)(typescript@5.6.2)': 1006 | dependencies: 1007 | '@rollup/pluginutils': 5.1.2(rollup@4.22.5) 1008 | resolve: 1.22.8 1009 | typescript: 5.6.2 1010 | optionalDependencies: 1011 | rollup: 4.22.5 1012 | tslib: 2.7.0 1013 | 1014 | '@rollup/pluginutils@5.1.2(rollup@4.22.5)': 1015 | dependencies: 1016 | '@types/estree': 1.0.6 1017 | estree-walker: 2.0.2 1018 | picomatch: 2.3.1 1019 | optionalDependencies: 1020 | rollup: 4.22.5 1021 | 1022 | '@rollup/rollup-android-arm-eabi@4.22.5': 1023 | optional: true 1024 | 1025 | '@rollup/rollup-android-arm64@4.22.5': 1026 | optional: true 1027 | 1028 | '@rollup/rollup-darwin-arm64@4.22.5': 1029 | optional: true 1030 | 1031 | '@rollup/rollup-darwin-x64@4.22.5': 1032 | optional: true 1033 | 1034 | '@rollup/rollup-linux-arm-gnueabihf@4.22.5': 1035 | optional: true 1036 | 1037 | '@rollup/rollup-linux-arm-musleabihf@4.22.5': 1038 | optional: true 1039 | 1040 | '@rollup/rollup-linux-arm64-gnu@4.22.5': 1041 | optional: true 1042 | 1043 | '@rollup/rollup-linux-arm64-musl@4.22.5': 1044 | optional: true 1045 | 1046 | '@rollup/rollup-linux-powerpc64le-gnu@4.22.5': 1047 | optional: true 1048 | 1049 | '@rollup/rollup-linux-riscv64-gnu@4.22.5': 1050 | optional: true 1051 | 1052 | '@rollup/rollup-linux-s390x-gnu@4.22.5': 1053 | optional: true 1054 | 1055 | '@rollup/rollup-linux-x64-gnu@4.22.5': 1056 | optional: true 1057 | 1058 | '@rollup/rollup-linux-x64-musl@4.22.5': 1059 | optional: true 1060 | 1061 | '@rollup/rollup-win32-arm64-msvc@4.22.5': 1062 | optional: true 1063 | 1064 | '@rollup/rollup-win32-ia32-msvc@4.22.5': 1065 | optional: true 1066 | 1067 | '@rollup/rollup-win32-x64-msvc@4.22.5': 1068 | optional: true 1069 | 1070 | '@types/estree@1.0.6': {} 1071 | 1072 | '@types/glob@7.2.0': 1073 | dependencies: 1074 | '@types/minimatch': 5.1.2 1075 | '@types/node': 22.7.4 1076 | 1077 | '@types/json-schema@7.0.15': {} 1078 | 1079 | '@types/minimatch@5.1.2': {} 1080 | 1081 | '@types/node@22.7.4': 1082 | dependencies: 1083 | undici-types: 6.19.8 1084 | 1085 | '@types/prop-types@15.7.13': {} 1086 | 1087 | '@types/react-dom@18.3.0': 1088 | dependencies: 1089 | '@types/react': 18.3.3 1090 | 1091 | '@types/react@18.3.3': 1092 | dependencies: 1093 | '@types/prop-types': 15.7.13 1094 | csstype: 3.1.3 1095 | 1096 | '@types/resolve@1.20.2': {} 1097 | 1098 | '@types/webpack@5.28.5': 1099 | dependencies: 1100 | '@types/node': 22.7.4 1101 | tapable: 2.2.1 1102 | webpack: 5.95.0 1103 | transitivePeerDependencies: 1104 | - '@swc/core' 1105 | - esbuild 1106 | - uglify-js 1107 | - webpack-cli 1108 | 1109 | '@webassemblyjs/ast@1.12.1': 1110 | dependencies: 1111 | '@webassemblyjs/helper-numbers': 1.11.6 1112 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 1113 | 1114 | '@webassemblyjs/floating-point-hex-parser@1.11.6': {} 1115 | 1116 | '@webassemblyjs/helper-api-error@1.11.6': {} 1117 | 1118 | '@webassemblyjs/helper-buffer@1.12.1': {} 1119 | 1120 | '@webassemblyjs/helper-numbers@1.11.6': 1121 | dependencies: 1122 | '@webassemblyjs/floating-point-hex-parser': 1.11.6 1123 | '@webassemblyjs/helper-api-error': 1.11.6 1124 | '@xtuc/long': 4.2.2 1125 | 1126 | '@webassemblyjs/helper-wasm-bytecode@1.11.6': {} 1127 | 1128 | '@webassemblyjs/helper-wasm-section@1.12.1': 1129 | dependencies: 1130 | '@webassemblyjs/ast': 1.12.1 1131 | '@webassemblyjs/helper-buffer': 1.12.1 1132 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 1133 | '@webassemblyjs/wasm-gen': 1.12.1 1134 | 1135 | '@webassemblyjs/ieee754@1.11.6': 1136 | dependencies: 1137 | '@xtuc/ieee754': 1.2.0 1138 | 1139 | '@webassemblyjs/leb128@1.11.6': 1140 | dependencies: 1141 | '@xtuc/long': 4.2.2 1142 | 1143 | '@webassemblyjs/utf8@1.11.6': {} 1144 | 1145 | '@webassemblyjs/wasm-edit@1.12.1': 1146 | dependencies: 1147 | '@webassemblyjs/ast': 1.12.1 1148 | '@webassemblyjs/helper-buffer': 1.12.1 1149 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 1150 | '@webassemblyjs/helper-wasm-section': 1.12.1 1151 | '@webassemblyjs/wasm-gen': 1.12.1 1152 | '@webassemblyjs/wasm-opt': 1.12.1 1153 | '@webassemblyjs/wasm-parser': 1.12.1 1154 | '@webassemblyjs/wast-printer': 1.12.1 1155 | 1156 | '@webassemblyjs/wasm-gen@1.12.1': 1157 | dependencies: 1158 | '@webassemblyjs/ast': 1.12.1 1159 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 1160 | '@webassemblyjs/ieee754': 1.11.6 1161 | '@webassemblyjs/leb128': 1.11.6 1162 | '@webassemblyjs/utf8': 1.11.6 1163 | 1164 | '@webassemblyjs/wasm-opt@1.12.1': 1165 | dependencies: 1166 | '@webassemblyjs/ast': 1.12.1 1167 | '@webassemblyjs/helper-buffer': 1.12.1 1168 | '@webassemblyjs/wasm-gen': 1.12.1 1169 | '@webassemblyjs/wasm-parser': 1.12.1 1170 | 1171 | '@webassemblyjs/wasm-parser@1.12.1': 1172 | dependencies: 1173 | '@webassemblyjs/ast': 1.12.1 1174 | '@webassemblyjs/helper-api-error': 1.11.6 1175 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 1176 | '@webassemblyjs/ieee754': 1.11.6 1177 | '@webassemblyjs/leb128': 1.11.6 1178 | '@webassemblyjs/utf8': 1.11.6 1179 | 1180 | '@webassemblyjs/wast-printer@1.12.1': 1181 | dependencies: 1182 | '@webassemblyjs/ast': 1.12.1 1183 | '@xtuc/long': 4.2.2 1184 | 1185 | '@xtuc/ieee754@1.2.0': {} 1186 | 1187 | '@xtuc/long@4.2.2': {} 1188 | 1189 | acorn-import-attributes@1.9.5(acorn@8.12.1): 1190 | dependencies: 1191 | acorn: 8.12.1 1192 | 1193 | acorn@8.12.1: {} 1194 | 1195 | aggregate-error@3.1.0: 1196 | dependencies: 1197 | clean-stack: 2.2.0 1198 | indent-string: 4.0.0 1199 | 1200 | ajv-keywords@3.5.2(ajv@6.12.6): 1201 | dependencies: 1202 | ajv: 6.12.6 1203 | 1204 | ajv@6.12.6: 1205 | dependencies: 1206 | fast-deep-equal: 3.1.3 1207 | fast-json-stable-stringify: 2.1.0 1208 | json-schema-traverse: 0.4.1 1209 | uri-js: 4.4.1 1210 | 1211 | ansi-regex@5.0.1: {} 1212 | 1213 | ansi-regex@6.1.0: {} 1214 | 1215 | ansi-styles@4.3.0: 1216 | dependencies: 1217 | color-convert: 2.0.1 1218 | 1219 | ansi-styles@6.2.1: {} 1220 | 1221 | array-union@2.1.0: {} 1222 | 1223 | balanced-match@1.0.2: {} 1224 | 1225 | brace-expansion@1.1.11: 1226 | dependencies: 1227 | balanced-match: 1.0.2 1228 | concat-map: 0.0.1 1229 | 1230 | brace-expansion@2.0.1: 1231 | dependencies: 1232 | balanced-match: 1.0.2 1233 | 1234 | braces@3.0.3: 1235 | dependencies: 1236 | fill-range: 7.1.1 1237 | 1238 | browserslist@4.24.0: 1239 | dependencies: 1240 | caniuse-lite: 1.0.30001664 1241 | electron-to-chromium: 1.5.29 1242 | node-releases: 2.0.18 1243 | update-browserslist-db: 1.1.1(browserslist@4.24.0) 1244 | 1245 | buffer-from@1.1.2: {} 1246 | 1247 | caniuse-lite@1.0.30001664: {} 1248 | 1249 | chrome-trace-event@1.0.4: {} 1250 | 1251 | clean-stack@2.2.0: {} 1252 | 1253 | color-convert@2.0.1: 1254 | dependencies: 1255 | color-name: 1.1.4 1256 | 1257 | color-name@1.1.4: {} 1258 | 1259 | commander@2.20.3: {} 1260 | 1261 | commondir@1.0.1: {} 1262 | 1263 | concat-map@0.0.1: {} 1264 | 1265 | cross-spawn@7.0.3: 1266 | dependencies: 1267 | path-key: 3.1.1 1268 | shebang-command: 2.0.0 1269 | which: 2.0.2 1270 | 1271 | csstype@3.1.3: {} 1272 | 1273 | deepmerge@4.3.1: {} 1274 | 1275 | del@5.1.0: 1276 | dependencies: 1277 | globby: 10.0.2 1278 | graceful-fs: 4.2.11 1279 | is-glob: 4.0.3 1280 | is-path-cwd: 2.2.0 1281 | is-path-inside: 3.0.3 1282 | p-map: 3.0.0 1283 | rimraf: 3.0.2 1284 | slash: 3.0.0 1285 | 1286 | dir-glob@3.0.1: 1287 | dependencies: 1288 | path-type: 4.0.0 1289 | 1290 | eastasianwidth@0.2.0: {} 1291 | 1292 | electron-to-chromium@1.5.29: {} 1293 | 1294 | emoji-regex@8.0.0: {} 1295 | 1296 | emoji-regex@9.2.2: {} 1297 | 1298 | enhanced-resolve@5.17.1: 1299 | dependencies: 1300 | graceful-fs: 4.2.11 1301 | tapable: 2.2.1 1302 | 1303 | es-module-lexer@1.5.4: {} 1304 | 1305 | escalade@3.2.0: {} 1306 | 1307 | eslint-scope@5.1.1: 1308 | dependencies: 1309 | esrecurse: 4.3.0 1310 | estraverse: 4.3.0 1311 | 1312 | esrecurse@4.3.0: 1313 | dependencies: 1314 | estraverse: 5.3.0 1315 | 1316 | estraverse@4.3.0: {} 1317 | 1318 | estraverse@5.3.0: {} 1319 | 1320 | estree-walker@0.6.1: {} 1321 | 1322 | estree-walker@2.0.2: {} 1323 | 1324 | estree-walker@3.0.3: 1325 | dependencies: 1326 | '@types/estree': 1.0.6 1327 | 1328 | events@3.3.0: {} 1329 | 1330 | fast-deep-equal@3.1.3: {} 1331 | 1332 | fast-glob@3.3.2: 1333 | dependencies: 1334 | '@nodelib/fs.stat': 2.0.5 1335 | '@nodelib/fs.walk': 1.2.8 1336 | glob-parent: 5.1.2 1337 | merge2: 1.4.1 1338 | micromatch: 4.0.8 1339 | 1340 | fast-json-stable-stringify@2.1.0: {} 1341 | 1342 | fastq@1.17.1: 1343 | dependencies: 1344 | reusify: 1.0.4 1345 | 1346 | fill-range@7.1.1: 1347 | dependencies: 1348 | to-regex-range: 5.0.1 1349 | 1350 | foreground-child@3.3.0: 1351 | dependencies: 1352 | cross-spawn: 7.0.3 1353 | signal-exit: 4.1.0 1354 | 1355 | fs.realpath@1.0.0: {} 1356 | 1357 | fsevents@2.3.3: 1358 | optional: true 1359 | 1360 | function-bind@1.1.2: {} 1361 | 1362 | glob-parent@5.1.2: 1363 | dependencies: 1364 | is-glob: 4.0.3 1365 | 1366 | glob-to-regexp@0.4.1: {} 1367 | 1368 | glob@10.4.5: 1369 | dependencies: 1370 | foreground-child: 3.3.0 1371 | jackspeak: 3.4.3 1372 | minimatch: 9.0.5 1373 | minipass: 7.1.2 1374 | package-json-from-dist: 1.0.1 1375 | path-scurry: 1.11.1 1376 | 1377 | glob@7.2.3: 1378 | dependencies: 1379 | fs.realpath: 1.0.0 1380 | inflight: 1.0.6 1381 | inherits: 2.0.4 1382 | minimatch: 3.1.2 1383 | once: 1.4.0 1384 | path-is-absolute: 1.0.1 1385 | 1386 | globby@10.0.2: 1387 | dependencies: 1388 | '@types/glob': 7.2.0 1389 | array-union: 2.1.0 1390 | dir-glob: 3.0.1 1391 | fast-glob: 3.3.2 1392 | glob: 7.2.3 1393 | ignore: 5.3.2 1394 | merge2: 1.4.1 1395 | slash: 3.0.0 1396 | 1397 | graceful-fs@4.2.11: {} 1398 | 1399 | has-flag@4.0.0: {} 1400 | 1401 | hasown@2.0.2: 1402 | dependencies: 1403 | function-bind: 1.1.2 1404 | 1405 | ignore@5.3.2: {} 1406 | 1407 | indent-string@4.0.0: {} 1408 | 1409 | inflight@1.0.6: 1410 | dependencies: 1411 | once: 1.4.0 1412 | wrappy: 1.0.2 1413 | 1414 | inherits@2.0.4: {} 1415 | 1416 | is-core-module@2.15.1: 1417 | dependencies: 1418 | hasown: 2.0.2 1419 | 1420 | is-extglob@2.1.1: {} 1421 | 1422 | is-fullwidth-code-point@3.0.0: {} 1423 | 1424 | is-glob@4.0.3: 1425 | dependencies: 1426 | is-extglob: 2.1.1 1427 | 1428 | is-module@1.0.0: {} 1429 | 1430 | is-number@7.0.0: {} 1431 | 1432 | is-path-cwd@2.2.0: {} 1433 | 1434 | is-path-inside@3.0.3: {} 1435 | 1436 | is-reference@1.2.1: 1437 | dependencies: 1438 | '@types/estree': 1.0.6 1439 | 1440 | is-reference@3.0.2: 1441 | dependencies: 1442 | '@types/estree': 1.0.6 1443 | 1444 | is-what@5.0.2: {} 1445 | 1446 | isexe@2.0.0: {} 1447 | 1448 | jackspeak@3.4.3: 1449 | dependencies: 1450 | '@isaacs/cliui': 8.0.2 1451 | optionalDependencies: 1452 | '@pkgjs/parseargs': 0.11.0 1453 | 1454 | jest-worker@27.5.1: 1455 | dependencies: 1456 | '@types/node': 22.7.4 1457 | merge-stream: 2.0.0 1458 | supports-color: 8.1.1 1459 | 1460 | js-tokens@4.0.0: {} 1461 | 1462 | json-parse-even-better-errors@2.3.1: {} 1463 | 1464 | json-schema-traverse@0.4.1: {} 1465 | 1466 | loader-runner@4.3.0: {} 1467 | 1468 | loose-envify@1.4.0: 1469 | dependencies: 1470 | js-tokens: 4.0.0 1471 | 1472 | lru-cache@10.4.3: {} 1473 | 1474 | magic-string@0.30.11: 1475 | dependencies: 1476 | '@jridgewell/sourcemap-codec': 1.5.0 1477 | 1478 | merge-anything@6.0.2: 1479 | dependencies: 1480 | is-what: 5.0.2 1481 | 1482 | merge-stream@2.0.0: {} 1483 | 1484 | merge2@1.4.1: {} 1485 | 1486 | micromatch@4.0.8: 1487 | dependencies: 1488 | braces: 3.0.3 1489 | picomatch: 2.3.1 1490 | 1491 | mime-db@1.52.0: {} 1492 | 1493 | mime-types@2.1.35: 1494 | dependencies: 1495 | mime-db: 1.52.0 1496 | 1497 | minimatch@3.1.2: 1498 | dependencies: 1499 | brace-expansion: 1.1.11 1500 | 1501 | minimatch@9.0.5: 1502 | dependencies: 1503 | brace-expansion: 2.0.1 1504 | 1505 | minipass@7.1.2: {} 1506 | 1507 | neo-async@2.6.2: {} 1508 | 1509 | node-releases@2.0.18: {} 1510 | 1511 | once@1.4.0: 1512 | dependencies: 1513 | wrappy: 1.0.2 1514 | 1515 | p-map@3.0.0: 1516 | dependencies: 1517 | aggregate-error: 3.1.0 1518 | 1519 | package-json-from-dist@1.0.1: {} 1520 | 1521 | path-is-absolute@1.0.1: {} 1522 | 1523 | path-key@3.1.1: {} 1524 | 1525 | path-parse@1.0.7: {} 1526 | 1527 | path-scurry@1.11.1: 1528 | dependencies: 1529 | lru-cache: 10.4.3 1530 | minipass: 7.1.2 1531 | 1532 | path-type@4.0.0: {} 1533 | 1534 | picocolors@1.1.0: {} 1535 | 1536 | picomatch@2.3.1: {} 1537 | 1538 | punycode@2.3.1: {} 1539 | 1540 | queue-microtask@1.2.3: {} 1541 | 1542 | randombytes@2.1.0: 1543 | dependencies: 1544 | safe-buffer: 5.2.1 1545 | 1546 | react-icons@5.3.0(react@18.3.1): 1547 | dependencies: 1548 | react: 18.3.1 1549 | 1550 | react@18.3.1: 1551 | dependencies: 1552 | loose-envify: 1.4.0 1553 | 1554 | resolve@1.22.8: 1555 | dependencies: 1556 | is-core-module: 2.15.1 1557 | path-parse: 1.0.7 1558 | supports-preserve-symlinks-flag: 1.0.0 1559 | 1560 | reusify@1.0.4: {} 1561 | 1562 | rimraf@3.0.2: 1563 | dependencies: 1564 | glob: 7.2.3 1565 | 1566 | rollup-plugin-delete@2.1.0(rollup@4.22.5): 1567 | dependencies: 1568 | del: 5.1.0 1569 | rollup: 4.22.5 1570 | 1571 | rollup-plugin-external-globals@0.11.0(rollup@4.22.5): 1572 | dependencies: 1573 | '@rollup/pluginutils': 5.1.2(rollup@4.22.5) 1574 | estree-walker: 3.0.3 1575 | is-reference: 3.0.2 1576 | magic-string: 0.30.11 1577 | rollup: 4.22.5 1578 | 1579 | rollup-plugin-import-assets@1.1.1(rollup@4.22.5): 1580 | dependencies: 1581 | rollup: 4.22.5 1582 | rollup-pluginutils: 2.8.2 1583 | url-join: 4.0.1 1584 | 1585 | rollup-pluginutils@2.8.2: 1586 | dependencies: 1587 | estree-walker: 0.6.1 1588 | 1589 | rollup@4.22.5: 1590 | dependencies: 1591 | '@types/estree': 1.0.6 1592 | optionalDependencies: 1593 | '@rollup/rollup-android-arm-eabi': 4.22.5 1594 | '@rollup/rollup-android-arm64': 4.22.5 1595 | '@rollup/rollup-darwin-arm64': 4.22.5 1596 | '@rollup/rollup-darwin-x64': 4.22.5 1597 | '@rollup/rollup-linux-arm-gnueabihf': 4.22.5 1598 | '@rollup/rollup-linux-arm-musleabihf': 4.22.5 1599 | '@rollup/rollup-linux-arm64-gnu': 4.22.5 1600 | '@rollup/rollup-linux-arm64-musl': 4.22.5 1601 | '@rollup/rollup-linux-powerpc64le-gnu': 4.22.5 1602 | '@rollup/rollup-linux-riscv64-gnu': 4.22.5 1603 | '@rollup/rollup-linux-s390x-gnu': 4.22.5 1604 | '@rollup/rollup-linux-x64-gnu': 4.22.5 1605 | '@rollup/rollup-linux-x64-musl': 4.22.5 1606 | '@rollup/rollup-win32-arm64-msvc': 4.22.5 1607 | '@rollup/rollup-win32-ia32-msvc': 4.22.5 1608 | '@rollup/rollup-win32-x64-msvc': 4.22.5 1609 | fsevents: 2.3.3 1610 | 1611 | run-parallel@1.2.0: 1612 | dependencies: 1613 | queue-microtask: 1.2.3 1614 | 1615 | safe-buffer@5.2.1: {} 1616 | 1617 | schema-utils@3.3.0: 1618 | dependencies: 1619 | '@types/json-schema': 7.0.15 1620 | ajv: 6.12.6 1621 | ajv-keywords: 3.5.2(ajv@6.12.6) 1622 | 1623 | serialize-javascript@6.0.2: 1624 | dependencies: 1625 | randombytes: 2.1.0 1626 | 1627 | shebang-command@2.0.0: 1628 | dependencies: 1629 | shebang-regex: 3.0.0 1630 | 1631 | shebang-regex@3.0.0: {} 1632 | 1633 | signal-exit@4.1.0: {} 1634 | 1635 | slash@3.0.0: {} 1636 | 1637 | source-map-support@0.5.21: 1638 | dependencies: 1639 | buffer-from: 1.1.2 1640 | source-map: 0.6.1 1641 | 1642 | source-map@0.6.1: {} 1643 | 1644 | string-width@4.2.3: 1645 | dependencies: 1646 | emoji-regex: 8.0.0 1647 | is-fullwidth-code-point: 3.0.0 1648 | strip-ansi: 6.0.1 1649 | 1650 | string-width@5.1.2: 1651 | dependencies: 1652 | eastasianwidth: 0.2.0 1653 | emoji-regex: 9.2.2 1654 | strip-ansi: 7.1.0 1655 | 1656 | strip-ansi@6.0.1: 1657 | dependencies: 1658 | ansi-regex: 5.0.1 1659 | 1660 | strip-ansi@7.1.0: 1661 | dependencies: 1662 | ansi-regex: 6.1.0 1663 | 1664 | supports-color@8.1.1: 1665 | dependencies: 1666 | has-flag: 4.0.0 1667 | 1668 | supports-preserve-symlinks-flag@1.0.0: {} 1669 | 1670 | tapable@2.2.1: {} 1671 | 1672 | terser-webpack-plugin@5.3.10(webpack@5.95.0): 1673 | dependencies: 1674 | '@jridgewell/trace-mapping': 0.3.25 1675 | jest-worker: 27.5.1 1676 | schema-utils: 3.3.0 1677 | serialize-javascript: 6.0.2 1678 | terser: 5.34.0 1679 | webpack: 5.95.0 1680 | 1681 | terser@5.34.0: 1682 | dependencies: 1683 | '@jridgewell/source-map': 0.3.6 1684 | acorn: 8.12.1 1685 | commander: 2.20.3 1686 | source-map-support: 0.5.21 1687 | 1688 | to-regex-range@5.0.1: 1689 | dependencies: 1690 | is-number: 7.0.0 1691 | 1692 | tslib@2.7.0: {} 1693 | 1694 | typescript@5.6.2: {} 1695 | 1696 | undici-types@6.19.8: {} 1697 | 1698 | update-browserslist-db@1.1.1(browserslist@4.24.0): 1699 | dependencies: 1700 | browserslist: 4.24.0 1701 | escalade: 3.2.0 1702 | picocolors: 1.1.0 1703 | 1704 | uri-js@4.4.1: 1705 | dependencies: 1706 | punycode: 2.3.1 1707 | 1708 | url-join@4.0.1: {} 1709 | 1710 | watchpack@2.4.2: 1711 | dependencies: 1712 | glob-to-regexp: 0.4.1 1713 | graceful-fs: 4.2.11 1714 | 1715 | webpack-sources@3.2.3: {} 1716 | 1717 | webpack@5.95.0: 1718 | dependencies: 1719 | '@types/estree': 1.0.6 1720 | '@webassemblyjs/ast': 1.12.1 1721 | '@webassemblyjs/wasm-edit': 1.12.1 1722 | '@webassemblyjs/wasm-parser': 1.12.1 1723 | acorn: 8.12.1 1724 | acorn-import-attributes: 1.9.5(acorn@8.12.1) 1725 | browserslist: 4.24.0 1726 | chrome-trace-event: 1.0.4 1727 | enhanced-resolve: 5.17.1 1728 | es-module-lexer: 1.5.4 1729 | eslint-scope: 5.1.1 1730 | events: 3.3.0 1731 | glob-to-regexp: 0.4.1 1732 | graceful-fs: 4.2.11 1733 | json-parse-even-better-errors: 2.3.1 1734 | loader-runner: 4.3.0 1735 | mime-types: 2.1.35 1736 | neo-async: 2.6.2 1737 | schema-utils: 3.3.0 1738 | tapable: 2.2.1 1739 | terser-webpack-plugin: 5.3.10(webpack@5.95.0) 1740 | watchpack: 2.4.2 1741 | webpack-sources: 3.2.3 1742 | transitivePeerDependencies: 1743 | - '@swc/core' 1744 | - esbuild 1745 | - uglify-js 1746 | 1747 | which@2.0.2: 1748 | dependencies: 1749 | isexe: 2.0.0 1750 | 1751 | wrap-ansi@7.0.0: 1752 | dependencies: 1753 | ansi-styles: 4.3.0 1754 | string-width: 4.2.3 1755 | strip-ansi: 6.0.1 1756 | 1757 | wrap-ansi@8.1.0: 1758 | dependencies: 1759 | ansi-styles: 6.2.1 1760 | string-width: 5.1.2 1761 | strip-ansi: 7.1.0 1762 | 1763 | wrappy@1.0.2: {} 1764 | --------------------------------------------------------------------------------