summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/release-on-tag.yml7
-rw-r--r--README.md59
-rw-r--r--decky_client.py (renamed from decky_plugin_installer.py)126
-rw-r--r--mock_decky_server.py76
-rwxr-xr-xtest.sh2
-rw-r--r--user_install_script.sh39
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
diff --git a/README.md b/README.md
index 3dda5ed..9747125 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/test.sh b/test.sh
index 1e69371..45f6859 100755
--- a/test.sh
+++ b/test.sh
@@ -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}"