diff options
| -rw-r--r-- | .github/workflows/release-on-tag.yml | 7 | ||||
| -rw-r--r-- | README.md | 59 | ||||
| -rw-r--r-- | decky_client.py (renamed from decky_plugin_installer.py) | 126 | ||||
| -rw-r--r-- | mock_decky_server.py | 76 | ||||
| -rwxr-xr-x | test.sh | 2 | ||||
| -rw-r--r-- | user_install_script.sh | 39 |
6 files changed, 295 insertions, 14 deletions
diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index fb8c021..92dc816 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -25,6 +25,10 @@ jobs: sed -i "s|__DECKY_PLUGIN_ID__|${PLUGIN_ID}|g" user_install_script.sh sed -i "s|__DECKY_MIRROR_HOST__|$MIRROR_HOST|g" decky_installer.desktop + - name: Generate checksum for decky_client.py + run: | + sha256sum decky_client.py > decky_client.py.sha256 + - name: Create GitHub release uses: softprops/action-gh-release@v2 with: @@ -32,5 +36,6 @@ jobs: tag_name: ${{ github.ref_name }} files: | user_install_script.sh - decky_plugin_installer.py + decky_client.py + decky_client.py.sha256 decky_installer.desktop @@ -7,11 +7,70 @@ A local mirror version of the Decky Installer for Steam Deck. This repository al - Local hosting of Decky Installer files - Easy installation of Decky plugins - No dependency on external servers +- **Custom store configuration support** - Configure and use custom plugin store URLs ## Usage +### Basic Plugin Installation + 1. Download the `user_install_script.sh` or the `decky_installer.desktop` file from the releases section. 2. Place the downloaded file in a convenient location on your Steam Deck. 3. Run the script or launch the desktop file to start the Decky Installer. +### Command Structure + +The `decky_client.py` script uses subcommands for different operations: + +```bash +# Install a plugin +python3 decky_client.py install [options] + +# Configure custom store URL +python3 decky_client.py configure-store <url> + +# Get configured store URL +python3 decky_client.py get-store +``` + +### Custom Store Configuration + +The installer now supports configuring custom plugin store URLs: + +#### Configure a Custom Store URL +```bash +python3 decky_client.py configure-store "https://your-custom-store.com/plugins" +``` + +#### Get the Currently Configured Store URL +```bash +python3 decky_client.py get-store +``` + +#### Install from a Custom Store +```bash +python3 decky_client.py install --target-id 42 --store-url "https://your-custom-store.com/plugins" +``` + +## Mock Server for Testing + +This repository includes a mock Decky Loader server for testing purposes: + +### Start the Mock Server +```bash +python3 mock_decky_server.py --auto-confirm +``` + +### Test with the Mock Server +```bash +python3 decky_client.py install --target-id 42 +``` + +The mock server implements the following Decky Loader backend routes: +- `utilities/ping` - Health check +- `utilities/install_plugin` - Plugin installation +- `utilities/confirm_plugin_install` - Confirm installation +- `utilities/cancel_plugin_install` - Cancel installation +- `utilities/settings/get` - Get configuration settings +- `utilities/settings/set` - Set configuration settings + diff --git a/decky_plugin_installer.py b/decky_client.py index 7c70235..f1ac2b6 100644 --- a/decky_plugin_installer.py +++ b/decky_client.py @@ -14,6 +14,9 @@ REPLY = 1 ERROR = -1 EVENT = 3 +# Default store URL +DEFAULT_STORE_URL = "https://plugins.deckbrew.xyz/plugins" + def log(*args: Any) -> None: """Print formatted logs to stderr.""" @@ -229,9 +232,124 @@ async def run_installer(target_id: int, store_url: str) -> None: raise RuntimeError("Installation did not complete successfully") +async def configure_store_url(store_url: str) -> None: + """Configure custom store URL in Decky settings.""" + client = DeckyClient() + try: + log(f"Connecting to Decky server at {client.host}:{client.port}...") + token = await client.get_token() + await client.connect(token) + + log(f"Setting custom store URL: {store_url}") + await client.send(CALL, "utilities/settings/set", ["store_url", store_url]) + + # Wait for reply + msg = await client.recv() + if msg is None: + raise RuntimeError("Connection closed by server") + + m_type = msg.get("type") + + if m_type == REPLY: + log(f"Store URL configured successfully: {msg.get('result')}") + elif m_type == ERROR: + log(f"Server error: {msg.get('error')}") + raise RuntimeError(f"Failed to set store URL: {msg.get('error')}") + + except Exception as e: + log(f"Error: {e}") + raise + finally: + await client.close() + + +async def get_store_url() -> str: + """Get the configured custom store URL from Decky settings.""" + client = DeckyClient() + try: + log(f"Connecting to Decky server at {client.host}:{client.port}...") + token = await client.get_token() + await client.connect(token) + + log("Getting configured store URL...") + await client.send(CALL, "utilities/settings/get", ["store_url", DEFAULT_STORE_URL]) + + # Wait for reply + msg = await client.recv() + if msg is None: + raise RuntimeError("Connection closed by server") + + m_type = msg.get("type") + + if m_type == REPLY: + store_url = msg.get('result') + log(f"Current store URL: {store_url}") + return store_url + elif m_type == ERROR: + log(f"Server error: {msg.get('error')}") + raise RuntimeError(f"Failed to get store URL: {msg.get('error')}") + + raise RuntimeError("Unexpected response type") + + except Exception as e: + log(f"Error: {e}") + raise + finally: + await client.close() + + if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Decky Plugin Installer") - parser.add_argument("--store-url", default="http://127.0.0.1:1337/plugins") - parser.add_argument("--target-id", type=int, default=42) + parser = argparse.ArgumentParser( + description="Decky Loader Client - Manage plugins and settings", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Install subcommand + install_parser = subparsers.add_parser( + "install", + help="Install a plugin from the store" + ) + install_parser.add_argument( + "--store-url", + default="http://127.0.0.1:1337/plugins", + help="Plugin store URL to fetch plugins from (default: http://127.0.0.1:1337/plugins)" + ) + install_parser.add_argument( + "--target-id", + type=int, + default=42, + help="Plugin ID to install (default: 42)" + ) + + # Configure store subcommand + config_parser = subparsers.add_parser( + "configure-store", + help="Configure custom store URL in Decky settings" + ) + config_parser.add_argument( + "url", + help="Custom store URL to configure" + ) + + # Get store subcommand + subparsers.add_parser( + "get-store", + help="Get the configured custom store URL" + ) + args = parser.parse_args() - asyncio.run(run_installer(**vars(args))) + + # Execute based on subcommand + if args.command == "install": + asyncio.run(run_installer( + target_id=args.target_id, + store_url=args.store_url + )) + elif args.command == "configure-store": + asyncio.run(configure_store_url(args.url)) + elif args.command == "get-store": + asyncio.run(get_store_url()) + else: + parser.print_help() + sys.exit(1) diff --git a/mock_decky_server.py b/mock_decky_server.py index 7a7341e..7a74dcd 100644 --- a/mock_decky_server.py +++ b/mock_decky_server.py @@ -42,6 +42,9 @@ CSRF_TOKEN = "decky-" + os.urandom(16).hex() # Plugin install requests storage (simulates PluginBrowser.install_requests) install_requests: Dict[str, Dict[str, str]] = {} +# Settings storage (simulates SettingsManager) +settings_store: Dict[str, Any] = {} + logging.basicConfig( level=logging.INFO, @@ -198,6 +201,8 @@ def handle_call_route( "utilities/install_plugin": handle_install_plugin, "utilities/confirm_plugin_install": handle_confirm_plugin_install, "utilities/cancel_plugin_install": handle_cancel_plugin_install, + "utilities/settings/get": handle_get_setting, + "utilities/settings/set": handle_set_setting, } if route not in routes: @@ -409,6 +414,77 @@ def handle_cancel_plugin_install( return None +def handle_get_setting( + sock: socket.socket, + args: List[Any], + config: Dict[str, bool] +) -> Any: + """Handle utilities/settings/get route. + + Corresponds to: + - utilities.py async def get_setting() (line 272) + - settings.py def getSetting() (line 58) + + Function signature from utilities.py: + async def get_setting(self, key: str, default: Any) + + Args: + sock: The WebSocket socket (unused). + args: [key, default]. + config: Server configuration (unused). + + Returns: + The setting value or the default if not found. + """ + if len(args) < 1: + raise ValueError("get_setting requires key argument") + + key = args[0] + default = args[1] if len(args) > 1 else None + + value = settings_store.get(key, default) + logger.info("[get_setting] key=%s, default=%s, value=%s", key, default, value) + + return value + + +def handle_set_setting( + sock: socket.socket, + args: List[Any], + config: Dict[str, bool] +) -> Any: + """Handle utilities/settings/set route. + + Corresponds to: + - utilities.py async def set_setting() (line 275) + - settings.py def setSetting() (line 61) + + Function signature from utilities.py: + async def set_setting(self, key: str, value: Any) + + Args: + sock: The WebSocket socket (unused). + args: [key, value]. + config: Server configuration (unused). + + Returns: + The value that was set. + + Raises: + ValueError: If key or value is missing. + """ + if len(args) < 2: + raise ValueError("set_setting requires key and value arguments") + + key = args[0] + value = args[1] + + settings_store[key] = value + logger.info("[set_setting] key=%s, value=%s", key, value) + + return value + + def _do_install(sock: socket.socket, artifact: str, name: str, version: str, hash_val: str) -> None: """Simulate the installation process with progress events. @@ -8,4 +8,4 @@ server_pid=$! echo "Mock Decky Server is running. Logs are being written to /tmp/mock_decky_server.log" trap "kill $server_pid" EXIT -python3 decky_plugin_installer.py +python3 decky_client.py install diff --git a/user_install_script.sh b/user_install_script.sh index c411321..609252e 100644 --- a/user_install_script.sh +++ b/user_install_script.sh @@ -34,13 +34,36 @@ if [ "$SKIP_DECKY_INSTALL" != true ]; then bash "${tmp_script}" fi -# Download and run decky plugin installer helper (mirror-hosted). -plugin_installer="/tmp/decky_plugin_installer.py" -if curl -fsSL "https://${DECKY_MIRROR_HOST}/AeroCore-IO/decky-installer/releases/latest/download/decky_plugin_installer.py" -o "${plugin_installer}"; then - python3 "${plugin_installer}" \ - --store-url "https://${DECKY_PLUGIN_MIRROR_HOST}/plugins" \ - --target-id "${DECKY_PLUGIN_TARGET_ID}" -else - echo "Failed to download decky installer helper script." >&2 +# Download and verify Decky Loader client (mirror-hosted). +decky_client="/tmp/decky_client.py" +decky_client_checksum="/tmp/decky_client.py.sha256" + +# Download the client script +if ! curl -fsSL "https://${DECKY_MIRROR_HOST}/AeroCore-IO/decky-installer/releases/latest/download/decky_client.py" -o "${decky_client}"; then + echo "Failed to download Decky Loader client script." >&2 + exit 1 +fi + +# Download the checksum file +if ! curl -fsSL "https://${DECKY_MIRROR_HOST}/AeroCore-IO/decky-installer/releases/latest/download/decky_client.py.sha256" -o "${decky_client_checksum}"; then + echo "Failed to download checksum file for Decky Loader client." >&2 exit 1 fi + +# Verify the checksum +if ! (cd /tmp && sha256sum -c decky_client.py.sha256); then + echo "Checksum verification failed for Decky Loader client. File may be compromised." >&2 + rm -f "${decky_client}" "${decky_client_checksum}" + exit 1 +fi + +# Install the plugin +python3 "${decky_client}" install \ + --store-url "https://${DECKY_PLUGIN_MIRROR_HOST}/plugins" \ + --target-id "${DECKY_PLUGIN_TARGET_ID}" + +# Configure the custom store URL for future use +python3 "${decky_client}" configure-store "https://${DECKY_PLUGIN_MIRROR_HOST}/plugins" + +# Clean up +rm -f "${decky_client}" "${decky_client_checksum}" |
