diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8c6765fa7..5ebf87df2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -234,7 +234,7 @@ jobs:
runs-on: ubuntu-22.04
- if: github.event_name == 'release'
+ if: github.event_name == 'release' && !startsWith(github.event.release.tag_name, 'synapsedesktopclient')
outputs:
sdist-package-name: ${{ steps.build-package.outputs.sdist-package-name }}
@@ -343,6 +343,105 @@ jobs:
# asset_content_type: application/zip
+ # build standalone desktop client artifacts for Windows and macOS on release
+ build-electron-desktop-clients:
+ needs: [test, pre-commit]
+ if: github.event_name == 'release' && startsWith(github.event.release.tag_name, 'synapsedesktopclient')
+
+ strategy:
+ matrix:
+ include:
+ # Windows builds
+ - os: windows-2022
+ platform: windows
+ python-version: '3.11'
+ artifact-name: synapse-desktop-client-windows-x64
+
+ # macOS builds
+ - os: macos-14
+ platform: macos
+ python-version: '3.11'
+ artifact-name: synapse-desktop-client-macos
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv and set the python version
+ uses: astral-sh/setup-uv@v6
+ with:
+ activate-environment: true
+ python-version: 3.13
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+ cache-dependency-path: synapse-electron/package-lock.json
+
+ - name: Install Python dependencies
+ shell: bash
+ run: |
+ uv pip install "pyinstaller>=6.14.0" "pyinstaller-hooks-contrib>=2024.0"
+ uv pip install -e ".[electron]"
+
+ # ensure that numpy c extensions are installed on windows
+ # https://stackoverflow.com/a/59346525
+ if [ "${{startsWith(runner.os, 'Windows')}}" == "true" ]; then
+ uv pip uninstall numpy
+ uv pip uninstall setuptools
+ uv pip install setuptools
+ uv pip install numpy
+ fi
+
+ - name: Install Node.js dependencies
+ shell: bash
+ run: |
+ cd synapse-electron
+ npm install
+
+ - name: Build using build scripts (Windows)
+ if: matrix.platform == 'windows'
+ shell: bash
+ run: |
+ # Set environment variable to skip dependency installation in build script
+ export SKIP_DEPENDENCY_INSTALL=1
+
+ # Use cmd to run the batch file with proper Windows syntax
+ cmd //c "build_electron_app.bat"
+
+ - name: Build using build scripts (macOS)
+ if: matrix.platform == 'macos'
+ shell: bash
+ run: |
+ # Set environment variable to skip dependency installation in build script
+ export SKIP_DEPENDENCY_INSTALL=1
+
+ chmod +x build_electron_app.sh
+ ./build_electron_app.sh macos
+
+ - name: List built files
+ shell: bash
+ run: |
+ echo "Built files in synapse-electron/dist:"
+ if [ -d "synapse-electron/dist" ]; then
+ ls -la synapse-electron/dist/
+ else
+ echo "No dist directory found"
+ fi
+
+ - name: Upload to GitHub Release
+ uses: softprops/action-gh-release@v1
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ tag_name: ${{ github.event.release.tag_name }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ files: |
+ synapse-electron/dist/*.exe
+ synapse-electron/dist/*.dmg
+
# re-download the built package to the appropriate pypi server.
# we upload prereleases to test.pypi.org and releases to pypi.org.
deploy:
diff --git a/.gitignore b/.gitignore
index c798f50b0..2cf1b53ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,7 @@ coverage.xml
.ipynb_checkpoints
*.ipynb
.env
+*.spec
+
+synapse-electron/node_modules
+synapse-electron/backend/build
diff --git a/build_electron_app.bat b/build_electron_app.bat
new file mode 100644
index 000000000..a969fbea5
--- /dev/null
+++ b/build_electron_app.bat
@@ -0,0 +1,155 @@
+@echo off
+REM Build script for Synapse Desktop Client (Electron + Python Backend)
+REM This script creates a complete packaged application with both frontend and backend
+REM Usage: build_electron_app.bat
+
+echo Building Synapse Desktop Client (Electron + Python Backend)...
+
+REM Ensure we're in the project root
+cd /d "%~dp0"
+
+REM Activate virtual environment if it exists
+if exist ".venv\Scripts\activate.bat" (
+ echo Activating virtual environment...
+ call .venv\Scripts\activate.bat
+) else (
+ echo Warning: Virtual environment not found at .venv\Scripts\activate.bat
+ echo Continuing with system Python...
+)
+
+REM Check required tools
+echo Checking required tools...
+where node >nul 2>nul
+if %ERRORLEVEL% NEQ 0 (
+ echo ERROR: Node.js is not installed or not in PATH
+ echo Please install Node.js from https://nodejs.org/
+ exit /b 1
+)
+
+where python >nul 2>nul
+if %ERRORLEVEL% NEQ 0 (
+ echo ERROR: Python is not installed or not in PATH
+ echo Please install Python from https://python.org/
+ exit /b 1
+)
+
+REM Install Python dependencies with electron extras
+if "%SKIP_DEPENDENCY_INSTALL%"=="1" (
+ echo Skipping Python dependency installation (CI mode)
+) else (
+ echo Installing Python dependencies...
+ where uv >nul 2>nul
+ if %ERRORLEVEL% EQU 0 (
+ echo Using uv package manager...
+ uv pip install "pyinstaller>=6.14.0" "pyinstaller-hooks-contrib>=2024.0"
+ uv pip install -e .[electron]
+ ) else (
+ echo Using pip package manager...
+ python -m pip install "pyinstaller>=6.14.0" "pyinstaller-hooks-contrib>=2024.0"
+ python -m pip install -e .[electron]
+ )
+ if errorlevel 1 (
+ echo ERROR: Failed to install Python dependencies
+ exit /b 1
+ )
+)
+
+@REM REM Verify PyInstaller version
+@REM echo Verifying PyInstaller installation...
+@REM python -c "import pyinstaller; print('PyInstaller version:', pyinstaller.__version__)"
+@REM if errorlevel 1 (
+@REM echo ERROR: PyInstaller not properly installed
+@REM exit /b 1
+@REM )
+
+REM Build Python backend with PyInstaller
+echo Building Python backend...
+cd synapse-electron\backend
+
+REM Clean previous builds
+if exist dist rmdir /s /q dist
+if exist build rmdir /s /q build
+if exist *.spec del *.spec
+
+REM Build using the same pattern as the working build
+pyinstaller ^
+ --onefile ^
+ --name "synapse-backend" ^
+ --collect-all=synapseclient ^
+ --collect-all=fastapi ^
+ --collect-all=uvicorn ^
+ --collect-all=starlette ^
+ --collect-all=pydantic ^
+ --collect-all=websockets ^
+ --paths "..\.." ^
+ --paths "..\..\synapseclient" ^
+ --console ^
+ server.py
+
+echo Checking if executable was created...
+if not exist "dist\synapse-backend.exe" (
+ echo ERROR: PyInstaller failed to create executable
+ echo Check the output above for errors
+ exit /b 1
+)
+
+if errorlevel 1 (
+ echo ERROR: Python backend build failed
+ exit /b 1
+)
+
+echo Python backend built successfully
+
+REM Go back to electron directory
+cd ..
+
+@REM REM Install Node.js dependencies
+@REM echo.
+@REM echo ========================================
+@REM echo Installing Node.js dependencies...
+@REM echo ========================================
+@REM echo DEBUG: About to run npm install
+@REM npm install --verbose
+@REM echo DEBUG: npm install command completed
+@REM set NPM_INSTALL_EXIT=%ERRORLEVEL%
+@REM echo DEBUG: npm install exit code: %NPM_INSTALL_EXIT%
+@REM REM Check if node_modules exists to verify successful install
+@REM if not exist "node_modules" (
+@REM echo ERROR: Failed to install Node.js dependencies - node_modules directory not found
+@REM exit /b 1
+@REM )
+@REM echo DEBUG: Node.js dependencies installed successfully
+@REM echo DEBUG: Continuing to Electron build step...
+
+REM Build Electron application
+echo.
+echo ========================================
+echo Building Electron application...
+echo ========================================
+echo DEBUG: About to run npm run dist
+echo Running: npm run dist
+npm run dist --verbose
+if errorlevel 1 (
+ echo ERROR: Electron build failed
+ echo Check the output above for details
+ exit /b 1
+)
+
+echo.
+echo Build complete!
+echo.
+echo Electron application packages are in: synapse-electron\dist\
+echo Python backend executable is in: synapse-electron\backend\dist\
+
+REM Show built files
+echo Built files:
+if exist dist (
+ dir /b dist\*.exe 2>nul
+ dir /b dist\*.dmg 2>nul
+ dir /b dist\*.AppImage 2>nul
+)
+
+echo.
+echo SUCCESS: Synapse Desktop Client built!
+
+pause
diff --git a/build_electron_app.sh b/build_electron_app.sh
new file mode 100644
index 000000000..c05fe2ff2
--- /dev/null
+++ b/build_electron_app.sh
@@ -0,0 +1,178 @@
+#!/bin/bash
+
+# Build script for Synapse Desktop Client (Electron + Python Backend)
+# This script creates a complete packaged application with both frontend and backend
+# Usage: ./build_electron_app.sh [platform]
+# Platforms: linux, macos, all
+
+set -e
+
+# Default to current platform if no argument provided
+TARGET_PLATFORM=${1:-"auto"}
+
+echo "Building Synapse Desktop Client (Electron + Python Backend)..."
+
+# Ensure we're in the project root
+cd "$(dirname "$0")"
+
+# Activate virtual environment if it exists
+if [ -f ".venv/bin/activate" ]; then
+ echo "Activating virtual environment..."
+ source .venv/bin/activate
+else
+ echo "Warning: Virtual environment not found at .venv/bin/activate"
+ echo "Continuing with system Python..."
+fi
+
+# Check required tools
+echo "Checking required tools..."
+if ! command -v node &> /dev/null; then
+ echo "ERROR: Node.js is not installed or not in PATH"
+ echo "Please install Node.js from https://nodejs.org/"
+ exit 1
+fi
+
+if ! command -v python3 &> /dev/null; then
+ echo "ERROR: Python is not installed or not in PATH"
+ echo "Please install Python from https://python.org/"
+ exit 1
+fi
+
+# Install Python dependencies with electron extras
+if [ "$SKIP_DEPENDENCY_INSTALL" = "1" ]; then
+ echo "Skipping Python dependency installation (CI mode)"
+else
+ echo "Installing Python dependencies..."
+ if command -v uv &> /dev/null; then
+ echo "Using uv package manager..."
+ uv pip install "pyinstaller>=6.14.0" "pyinstaller-hooks-contrib>=2024.0"
+ uv pip install -e ".[electron]"
+ else
+ echo "Using pip package manager..."
+ python3 -m pip install "pyinstaller>=6.14.0" "pyinstaller-hooks-contrib>=2024.0"
+ python3 -m pip install -e ".[electron]"
+ fi
+fi
+
+# Verify PyInstaller version
+# echo "Verifying PyInstaller installation..."
+# python3 -c "import PyInstaller; print('PyInstaller version:', PyInstaller.__version__)"
+# if [ $? -ne 0 ]; then
+# echo "ERROR: PyInstaller not properly installed"
+# exit 1
+# fi
+
+# Function to build Python backend for a specific platform
+build_python_backend() {
+ local platform=$1
+
+ echo "Building Python backend for $platform..."
+ cd synapse-electron/backend
+
+ # Clean previous builds
+ rm -rf dist/ build/ *.spec
+
+ # Create PyInstaller spec and build
+ pyinstaller \
+ --onefile \
+ --name "synapse-backend" \
+ --collect-all=synapseclient \
+ --collect-all=fastapi \
+ --collect-all=uvicorn \
+ --collect-all=starlette \
+ --collect-all=pydantic \
+ --collect-all=websockets \
+ --paths "../.." \
+ --paths "../../synapseclient" \
+ --console \
+ server.py
+
+ if [ ! -f "dist/synapse-backend" ]; then
+ echo "ERROR: Python backend build failed"
+ exit 1
+ fi
+
+ echo "Python backend built successfully"
+ cd ../..
+}
+
+# Function to build Electron app for a specific platform
+build_electron_app() {
+ local platform=$1
+
+ echo "Building Electron application for $platform..."
+ cd synapse-electron
+
+ # # Install Node.js dependencies
+ # echo "Installing Node.js dependencies..."
+ # npm install
+
+ # Set platform-specific build command
+ case "$platform" in
+ "linux")
+ npm run build -- --linux
+ ;;
+ "macos")
+ npm run build -- --mac
+ ;;
+ "windows")
+ npm run build -- --win
+ ;;
+ *)
+ npm run build
+ ;;
+ esac
+
+ cd ..
+}
+
+# Determine what to build
+case "$TARGET_PLATFORM" in
+ "auto")
+ # Auto-detect current platform
+ if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+ build_python_backend "linux"
+ build_electron_app "linux"
+ elif [[ "$OSTYPE" == "darwin"* ]]; then
+ build_python_backend "macos"
+ build_electron_app "macos"
+ else
+ echo "Unsupported platform: $OSTYPE"
+ echo "This script supports Linux and macOS platforms"
+ echo "Please specify platform: linux, macos, or all"
+ exit 1
+ fi
+ ;;
+ "linux")
+ build_python_backend "linux"
+ build_electron_app "linux"
+ ;;
+ "macos")
+ build_python_backend "macos"
+ build_electron_app "macos"
+ ;;
+ "all")
+ echo "Building for all supported platforms..."
+ build_python_backend "linux"
+ build_python_backend "macos"
+ build_electron_app "linux"
+ build_electron_app "macos"
+ ;;
+ *)
+ echo "Unknown platform: $TARGET_PLATFORM"
+ echo "Available platforms: linux, macos, all"
+ exit 1
+ ;;
+esac
+
+echo ""
+echo "Build(s) complete!"
+echo ""
+echo "Electron application packages are in: synapse-electron/dist/"
+echo "Python backend executables are in: synapse-electron/backend/dist/"
+
+echo ""
+echo "Available packages:"
+if [ -d "synapse-electron/dist" ]; then
+ ls -la synapse-electron/dist/
+fi
diff --git a/setup.cfg b/setup.cfg
index 70e961735..68f14ca77 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -122,6 +122,16 @@ docs =
mkdocs-open-in-new-tab~=1.0.3
markdown-include~=0.8.1
+electron =
+ # For building the Electron app
+ pyinstaller>=6.14.0,<7.0
+ pyinstaller-hooks-contrib>=2024.0,<2026.0
+ # Backend dependencies
+ fastapi>=0.104.1
+ pydantic>=2.5.0
+ python-multipart>=0.0.6
+ uvicorn>=0.24.0
+ websockets>=12.0
[options.entry_points]
console_scripts =
diff --git a/synapse-electron/.electron-builder.json b/synapse-electron/.electron-builder.json
new file mode 100644
index 000000000..c5115b09f
--- /dev/null
+++ b/synapse-electron/.electron-builder.json
@@ -0,0 +1,61 @@
+{
+ "name": "Synapse Desktop Client",
+ "version": "1.0.0",
+ "description": "Development configuration for Synapse Desktop Client",
+ "main": "main.js",
+ "scripts": {
+ "electron": "electron .",
+ "electron-dev": "ELECTRON_IS_DEV=1 electron .",
+ "electron-pack": "electron-builder",
+ "preelectron-pack": "npm run build"
+ },
+ "build": {
+ "appId": "com.sagebase.synapse-desktop-client",
+ "productName": "Synapse Desktop Client",
+ "copyright": "Copyright © 2023 Sage Bionetworks",
+ "directories": {
+ "output": "dist"
+ },
+ "files": [
+ "**/*",
+ "!backend/__pycache__",
+ "!backend/*.pyc",
+ "!backend/venv",
+ "!.git",
+ "!README.md"
+ ],
+ "mac": {
+ "category": "public.app-category.developer-tools",
+ "target": [
+ {
+ "target": "dmg",
+ "arch": ["x64", "arm64"]
+ }
+ ]
+ },
+ "win": {
+ "target": [
+ {
+ "target": "nsis",
+ "arch": ["x64", "ia32"]
+ }
+ ]
+ },
+ "linux": {
+ "target": [
+ {
+ "target": "AppImage",
+ "arch": ["x64"]
+ },
+ {
+ "target": "deb",
+ "arch": ["x64"]
+ }
+ ]
+ }
+ },
+ "devDependencies": {
+ "electron": "^27.0.0",
+ "electron-builder": "^24.6.4"
+ }
+}
diff --git a/synapse-electron/README.md b/synapse-electron/README.md
new file mode 100644
index 000000000..460af9950
--- /dev/null
+++ b/synapse-electron/README.md
@@ -0,0 +1,266 @@
+# Synapse Desktop Client
+
+ElectronJS desktop application for interacting with Synapse, built with FastAPI backend and modern web frontend.
+
+## Architecture
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ Frontend │ │ Electron │ │ Backend │
+│ (HTML/CSS/JS) │◄───┤ Main Process │◄───┤ (FastAPI) │
+│ │ │ │ │ │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+ │ │
+ │ │
+ ▼ ▼
+ ┌─────────────────┐ ┌─────────────────┐
+ │ Renderer │ │ Synapse │
+ │ Process │ │ Python Client │
+ │ │ │ │
+ └─────────────────┘ └─────────────────┘
+```
+
+- **Frontend**: Modern web UI (HTML/CSS/JavaScript)
+- **Electron**: Cross-platform desktop wrapper
+- **FastAPI Backend**: Python REST API server
+- **Synapse Client**: Official Synapse Python SDK integration
+
+## Quick Start
+
+### Prerequisites
+
+Before running the Synapse Desktop Client, ensure you have the following system dependencies installed:
+
+#### Required Software
+- **Node.js** (v14 or higher) - Download from [https://nodejs.org/](https://nodejs.org/)
+ - Includes npm (Node Package Manager)
+- **Python** (v3.7 or higher) - Download from [https://python.org/](https://python.org/)
+ - On Windows: Use `python` command
+ - On macOS/Linux: Use `python3` command
+
+#### Environment Setup
+- Ensure both Node.js and Python are added to your system PATH
+- The start scripts will automatically check for these dependencies and guide you if anything is missing
+
+### Development
+
+#### Quick Start
+```bash
+# Make sure you are in the `synapse-electron` directory
+# cd synapse-electron
+
+# Windows
+.\start.bat
+
+# macOS/Linux
+./start.sh
+```
+
+After starting the application you should see a few things. First, you should see console logs similar to what is shown below:
+
+
+```
+Starting Synapse Desktop Client...
+Backend will start on http://localhost:8000
+WebSocket will start on ws://localhost:8001
+
+
+> synapse-desktop-client@0.1.0 dev
+> electron .
+
+
+12:22:17.807 > Starting Python backend...
+12:22:17.821 > Attempt 1/30: Checking backend health at http://127.0.0.1:8000/health
+12:22:17.841 > Backend not ready yet (attempt 1): connect ECONNREFUSED 127.0.0.1:8000
+12:22:18.847 > Attempt 2/30: Checking backend health at http://127.0.0.1:8000/health
+12:22:18.850 > Backend not ready yet (attempt 2): connect ECONNREFUSED 127.0.0.1:8000
+12:22:19.852 > Attempt 3/30: Checking backend health at http://127.0.0.1:8000/health
+12:22:19.856 > Python Backend: INFO: 127.0.0.1:52661 - "GET /health HTTP/1.1" 200 OK
+
+12:22:19.858 > Python backend is ready
+12:22:19.971 > Synapse Electron app initialized successfully
+12:22:19.988 > WebSocket connected to Python backend
+```
+
+Secondly, an application window should pop up allowing you to interact with the GUI.
+
+#### Advanced Development & Debugging
+
+For developers who need to debug the underlying Python code or JavaScript frontend, follow these detailed instructions:
+
+##### Python Backend Debugging
+
+To debug the FastAPI backend server with breakpoints and step-through debugging:
+
+1. **Start the Python backend manually** (before running the start script):
+ ```bash
+ # Navigate to the backend directory
+ cd backend
+
+ # Run the FastAPI server directly - Or through your regular debug session in VSCode
+ python server.py
+ ```
+
+ The backend will start on `http://localhost:8000` by default.
+
+ The FastAPI server also automatically exposes API Docs at the `http://localhost:8000/docs` URL.
+
+2. **Set up your IDE for Python debugging**:
+ - **VS Code**: Open `backend/server.py`, set breakpoints, and use F5 to start debugging
+ - **Other IDEs**: Configure to run `server.py` with your Python interpreter
+
+3. **Start the Electron frontend** (after the backend is running):
+ ```bash
+ # From the synapse-electron root directory
+ npm start
+ ```
+
+4. **Test your application** - breakpoints will be hit in both environments
+
+This setup allows you to:
+- Step through Python code in your IDE
+- Inspect FastAPI request/response cycles
+- Debug JavaScript interactions in the Chromium console
+- Test the full application flow with complete debugging capabilities
+
+**Important**: If you run `server.py` manually, you may still use the `start.bat`/`start.sh` scripts as. It will skip creating it's own Python process, however, the order of this is important. If you run the `start.bat`/`start.sh` scripts first it conflict with your debugging session (port already in use).
+
+##### JavaScript Frontend Debugging
+
+The ElectronJS application provides access to Chromium's developer tools:
+
+1. **Open Developer Console**:
+ - In the running Electron app, press `Ctrl+Shift+I` (Windows/Linux) or `Cmd+Option+I` (macOS)
+
+2. **Set breakpoints in JavaScript**:
+ - Open the Sources tab in Developer Tools
+ - Navigate to your JavaScript files (`app.js`, etc.)
+ - Click on line numbers to set breakpoints
+ - Use `debugger;` statements in your code for programmatic breakpoints
+
+### Production Build
+```bash
+npm run build # All platforms
+npm run dist:win # Windows only
+npm run dist:mac # macOS only
+npm run dist:linux # Linux only
+```
+
+## Project Structure
+
+```
+synapse-electron/
+├── src/ # Frontend source code
+│ ├── index.html # Main UI layout
+│ ├── app.js # Frontend logic
+│ └── styles.css # UI styling
+├── backend/ # Python FastAPI server
+│ ├── server.py # Main FastAPI application
+│ ├── models/ # Pydantic data models
+│ ├── services/ # Business logic layer
+│ └── utils/ # Shared utilities
+├── assets/ # Static resources
+├── main.js # Electron main process
+├── preload.js # Electron security bridge
+├── package.json # Node.js dependencies
+├── .electron-builder.json # Electron Builder configuration
+├── start.bat/.sh # Development startup scripts
+└── README.md # This file
+```
+
+## Directory Details
+
+### `/src/` - Frontend
+- **index.html**: Main application UI with login, file browser, and upload/download interfaces
+- **app.js**: JavaScript application logic, API communication, UI event handling
+- **styles.css**: CSS styling for modern, responsive interface
+
+### `/backend/` - Python API Server
+- **server.py**: FastAPI application with authentication, file operations, and system endpoints
+- **models/**: Pydantic models for request/response validation
+- **services/**: Core business logic (authentication, file operations, Synapse integration)
+- **utils/**: Logging, WebSocket, and system utilities
+
+### Root Files
+- **main.js**: Electron main process - window management, backend lifecycle
+- **preload.js**: Secure communication bridge between renderer and main process
+- **start.bat/.sh**: Development scripts that handle backend startup and Electron launch
+
+## Development Workflow
+
+1. **Backend**: FastAPI server runs on `http://localhost:8000`
+2. **Frontend**: Electron renderer loads from local HTML/CSS/JS
+3. **Communication**: REST API calls from frontend to backend
+4. **Authentication**: Token-based auth with Synapse
+5. **File Operations**: Upload/download through Synapse Python SDK
+
+## Key Features
+
+- **Authentication**: Username/token and profile-based login
+- **File Browser**: Navigate Synapse projects and folders
+- **Bulk Operations**: Multi-file upload/download with progress tracking
+- **Cross-Platform**: Windows, macOS, Linux support
+- **Real-time Logging**: WebSocket-based log streaming to UI
+
+## Requirements
+
+- **Node.js**: 16+ for Electron
+- **Python**: 3.8+ for backend
+- **Dependencies**: Managed via npm (frontend) and pip (backend)
+
+## Build Process
+
+1. **Development**: Scripts handle both backend and frontend startup
+2. **Production**: Electron Builder packages Python backend as executable
+3. **Distribution**: Creates platform-specific installers/packages
+
+## Configuration
+
+- **Backend Port**: Configured in `backend/server.py` (default: 8000)
+- **Build Settings**: Defined in package.json and `.electron-builder.json`
+- **Python Environment**: Managed in `backend/` directory
+
+## Releasing
+
+When incrementing the desktop application for a release, there are **2 versions** that need to be updated:
+
+1. **`package.json`** - Update the `version` field:
+ ```json
+ {
+ "name": "synapse-desktop-client",
+ "version": "X.Y.Z",
+ ...
+ }
+ ```
+
+2. **`backend/server.py`** - Update the FastAPI `version` parameter:
+ ```python
+ app = FastAPI(
+ title="Synapse Desktop Client API",
+ description="Backend API for Synapse Desktop Client",
+ version="X.Y.Z",
+ lifespan=lifespan,
+ )
+ ```
+
+### Release Process
+
+1. **Create Release Branch**: Create a new release candidate branch from the develop branch using the release candidate naming convention:
+ ```bash
+ git checkout -b synapsedesktopclient/vX.Y.Z-rc develop
+ ```
+ where `X.Y.Z` is the semantic version (e.g., `synapsedesktopclient/v1.2.3-rc`)
+
+2. **Update Versions**: Update both version locations mentioned above to match the release version.
+
+3. **Pre-release Distribution**: At this point in time we will only use the "pre-release" portion to push out the desktop client. This is temporary while we look to migrate to the sage monorepo.
+
+4. **Create and Deploy the Release Candidate**: Desktop client releases are created as GitHub releases.
+
+ - Click the "Draft a new release" button and fill the following values:
+ - **Tag version**: `synapsedesktopclient/vX.Y.Z-rc` where `X.Y.Z` is the semantic version
+ - **Target**: the previously created `synapsedesktopclient/vX.Y.Z-rc` branch
+ - **Release title**: Same as tag version (`synapsedesktopclient/vX.Y.Z-rc`)
+ - **IMPORTANT**: Check the "Set as a pre-release" checkbox
+
+**Note**: Replace `X.Y.Z` with the actual semantic version numbers (e.g., `1.2.3`).
diff --git a/synapse-electron/assets/icon.icns b/synapse-electron/assets/icon.icns
new file mode 100644
index 000000000..40b8e1801
Binary files /dev/null and b/synapse-electron/assets/icon.icns differ
diff --git a/synapse-electron/assets/icon.ico b/synapse-electron/assets/icon.ico
new file mode 100644
index 000000000..8c36dc4a7
Binary files /dev/null and b/synapse-electron/assets/icon.ico differ
diff --git a/synapse-electron/assets/icon.png b/synapse-electron/assets/icon.png
new file mode 100644
index 000000000..b45f4369c
Binary files /dev/null and b/synapse-electron/assets/icon.png differ
diff --git a/synapse-electron/backend/hook-uvicorn.py b/synapse-electron/backend/hook-uvicorn.py
new file mode 100644
index 000000000..fb7a78843
--- /dev/null
+++ b/synapse-electron/backend/hook-uvicorn.py
@@ -0,0 +1,37 @@
+# PyInstaller hook for uvicorn
+# This helps ensure all uvicorn modules are properly included
+
+from PyInstaller.utils.hooks import collect_all, collect_submodules
+
+# Collect all uvicorn modules
+datas, binaries, hiddenimports = collect_all("uvicorn")
+
+# Add specific hidden imports that are often missed
+hiddenimports += [
+ "uvicorn.main",
+ "uvicorn.server",
+ "uvicorn.config",
+ "uvicorn.lifespan",
+ "uvicorn.lifespan.on",
+ "uvicorn.loops",
+ "uvicorn.loops.auto",
+ "uvicorn.loops.asyncio",
+ "uvicorn.loops.uvloop",
+ "uvicorn.protocols",
+ "uvicorn.protocols.http",
+ "uvicorn.protocols.http.auto",
+ "uvicorn.protocols.http.h11_impl",
+ "uvicorn.protocols.http.httptools_impl",
+ "uvicorn.protocols.websockets",
+ "uvicorn.protocols.websockets.auto",
+ "uvicorn.protocols.websockets.websockets_impl",
+ "uvicorn.protocols.websockets.wsproto_impl",
+ "uvicorn.supervisors",
+ "uvicorn.supervisors.basereload",
+ "uvicorn.supervisors.statreload",
+ "uvicorn.supervisors.watchgodreload",
+ "uvicorn.supervisors.watchfilesreload",
+]
+
+# Add all submodules
+hiddenimports += collect_submodules("uvicorn")
diff --git a/synapse-electron/backend/models/__init__.py b/synapse-electron/backend/models/__init__.py
new file mode 100644
index 000000000..a50f961b6
--- /dev/null
+++ b/synapse-electron/backend/models/__init__.py
@@ -0,0 +1,81 @@
+"""
+Models package for Synapse Desktop Client backend.
+
+This package contains all data models and schemas used in the application.
+Data models are pure data structures without business logic.
+"""
+
+from .api_models import (
+ AuthResponse,
+ BulkDownloadRequest,
+ BulkDownloadResponse,
+ BulkItem,
+ BulkItemsResponse,
+ BulkUploadRequest,
+ BulkUploadResponse,
+ CompletionMessage,
+ ConnectionStatusMessage,
+ DirectoryInfo,
+ DownloadRequest,
+ DownloadStartResponse,
+ EnumerateRequest,
+ EnumerateResponse,
+ ErrorResponse,
+ FileInfo,
+ HealthCheckResponse,
+ LoginRequest,
+ LogMessage,
+ LogoutResponse,
+ LogPollResponse,
+ OperationResponse,
+ ProfileInfo,
+ ProfilesResponse,
+ ProgressMessage,
+ ScanDirectoryRequest,
+ ScanDirectoryResponse,
+ ScanSummary,
+ TestLoggingResponse,
+ UploadRequest,
+ UploadResult,
+ UploadStartResponse,
+)
+from .domain_models import BulkItem as BulkItemModel
+
+__all__ = [
+ # API Request Models
+ "LoginRequest",
+ "DownloadRequest",
+ "UploadRequest",
+ "EnumerateRequest",
+ "BulkDownloadRequest",
+ "BulkUploadRequest",
+ "ScanDirectoryRequest",
+ # API Response Models
+ "AuthResponse",
+ "OperationResponse",
+ "DirectoryInfo",
+ "ProfileInfo",
+ "ProfilesResponse",
+ "BulkItemsResponse",
+ "FileInfo",
+ "ScanSummary",
+ "ScanDirectoryResponse",
+ "BulkItem",
+ "EnumerateResponse",
+ "UploadResult",
+ "BulkUploadResponse",
+ "LogMessage",
+ "LogPollResponse",
+ "HealthCheckResponse",
+ "TestLoggingResponse",
+ "DownloadStartResponse",
+ "UploadStartResponse",
+ "LogoutResponse",
+ "BulkDownloadResponse",
+ "CompletionMessage",
+ "ProgressMessage",
+ "ConnectionStatusMessage",
+ "ErrorResponse",
+ # Domain Models
+ "BulkItemModel",
+]
diff --git a/synapse-electron/backend/models/api_models.py b/synapse-electron/backend/models/api_models.py
new file mode 100644
index 000000000..9ed0b858b
--- /dev/null
+++ b/synapse-electron/backend/models/api_models.py
@@ -0,0 +1,296 @@
+"""
+API request and response models for the Synapse Desktop Client backend.
+
+This module contains all Pydantic models used for API request validation
+and response serialization.
+"""
+
+from typing import Any, Dict, List, Optional
+
+from pydantic import BaseModel, Field
+
+
+class LoginRequest(BaseModel):
+ """Request model for user authentication."""
+
+ mode: str = Field(..., description="Authentication mode: 'manual' or 'config'")
+ username: Optional[str] = Field(
+ None, description="Username for manual authentication"
+ )
+ token: Optional[str] = Field(
+ None, description="Authentication token for manual login"
+ )
+ profile: Optional[str] = Field(
+ None, description="Profile name for config-based authentication"
+ )
+
+
+class DownloadRequest(BaseModel):
+ """Request model for single file download."""
+
+ synapse_id: str = Field(..., description="Synapse entity ID to download")
+ version: Optional[str] = Field(None, description="Specific version to download")
+ download_path: str = Field(..., description="Local directory path for download")
+
+
+class UploadRequest(BaseModel):
+ """Request model for single file upload."""
+
+ file_path: str = Field(..., description="Local path to the file to upload")
+ mode: str = Field(..., description="Upload mode: 'new' or 'update'")
+ parent_id: Optional[str] = Field(None, description="Parent entity ID for new files")
+ entity_id: Optional[str] = Field(
+ None, description="Entity ID to update for existing files"
+ )
+ name: Optional[str] = Field(None, description="Name for the entity")
+
+
+class EnumerateRequest(BaseModel):
+ """Request model for container enumeration."""
+
+ container_id: str = Field(
+ ..., description="Synapse ID of the container to enumerate"
+ )
+ recursive: bool = Field(True, description="Whether to enumerate recursively")
+
+
+class BulkDownloadRequest(BaseModel):
+ """Request model for bulk file download."""
+
+ items: List[Dict[str, Any]] = Field(..., description="List of items to download")
+ download_path: str = Field(..., description="Local directory path for downloads")
+ create_subfolders: bool = Field(True, description="Whether to create subfolders")
+
+
+class BulkUploadRequest(BaseModel):
+ """Request model for bulk file upload."""
+
+ parent_id: str = Field(..., description="Parent entity ID for uploads")
+ files: List[Dict[str, Any]] = Field(
+ ..., description="List of file objects to upload"
+ )
+ preserve_folder_structure: bool = Field(
+ True, description="Whether to preserve folder structure"
+ )
+
+
+class ScanDirectoryRequest(BaseModel):
+ """Request model for directory scanning."""
+
+ directory_path: str = Field(..., description="Directory path to scan")
+ recursive: bool = Field(True, description="Whether to scan recursively")
+
+
+class AuthResponse(BaseModel):
+ """Response model for authentication."""
+
+ username: str = Field(..., description="Authenticated username")
+ name: str = Field(..., description="Display name")
+ user_id: str = Field(..., description="User ID")
+ token: str = Field(..., description="Authentication status")
+
+
+class OperationResponse(BaseModel):
+ """Generic response model for operations."""
+
+ success: bool = Field(..., description="Whether the operation was successful")
+ message: Optional[str] = Field(None, description="Operation message")
+ error: Optional[str] = Field(None, description="Error message if operation failed")
+
+
+class DirectoryInfo(BaseModel):
+ """Response model for directory information."""
+
+ home_directory: str = Field(..., description="User's home directory path")
+ downloads_directory: str = Field(..., description="User's downloads directory path")
+
+
+class ProfileInfo(BaseModel):
+ """Model for authentication profile information."""
+
+ name: str = Field(..., description="Profile name")
+ display_name: str = Field(..., description="Profile display name")
+
+
+class ProfilesResponse(BaseModel):
+ """Response model for available authentication profiles."""
+
+ profiles: List[ProfileInfo] = Field(..., description="List of available profiles")
+
+
+class BulkItemsResponse(BaseModel):
+ """Response model for bulk items listing."""
+
+ items: List[Dict[str, Any]] = Field(..., description="List of bulk items")
+
+
+class FileInfo(BaseModel):
+ """Model for file information during directory scanning."""
+
+ id: str = Field(..., description="File identifier")
+ name: str = Field(..., description="File name")
+ type: str = Field(..., description="Item type: 'file' or 'folder'")
+ size: int = Field(..., description="File size in bytes")
+ path: str = Field(..., description="Full file path")
+ relative_path: str = Field(..., description="Relative path from scan root")
+ parent_path: str = Field(..., description="Parent directory path")
+ mime_type: Optional[str] = Field(None, description="MIME type of the file")
+
+
+class ScanSummary(BaseModel):
+ """Summary information for directory scanning."""
+
+ total_items: int = Field(..., description="Total number of items found")
+ file_count: int = Field(..., description="Number of files found")
+ folder_count: int = Field(..., description="Number of folders found")
+ total_size: int = Field(..., description="Total size of all files in bytes")
+
+
+class ScanDirectoryResponse(BaseModel):
+ """Response model for directory scanning."""
+
+ success: bool = Field(..., description="Whether the scan was successful")
+ files: List[FileInfo] = Field(..., description="List of files found")
+ summary: ScanSummary = Field(..., description="Summary of scan results")
+
+
+class BulkItem(BaseModel):
+ """Model for items in bulk operations."""
+
+ id: str = Field(..., description="Item identifier")
+ name: str = Field(..., description="Item name")
+ type: str = Field(..., description="Item type")
+ size: Optional[int] = Field(None, description="Item size in bytes")
+ parent_id: Optional[str] = Field(None, description="Parent item ID")
+ path: str = Field("", description="Item path")
+
+
+class EnumerateResponse(BaseModel):
+ """Response model for container enumeration."""
+
+ items: List[BulkItem] = Field(..., description="List of enumerated items")
+
+
+class UploadResult(BaseModel):
+ """Model for individual upload results."""
+
+ success: bool = Field(..., description="Whether the upload was successful")
+ item_name: str = Field(..., description="Name of the uploaded item")
+ item_type: str = Field(..., description="Type of the uploaded item")
+ entity_id: Optional[str] = Field(None, description="Entity ID if successful")
+ error: Optional[str] = Field(None, description="Error message if failed")
+ path: Optional[str] = Field(None, description="Local file path")
+
+
+class BulkUploadResponse(BaseModel):
+ """Response model for bulk upload operations."""
+
+ success: bool = Field(..., description="Whether the operation was successful")
+ message: str = Field(..., description="Operation summary message")
+ successful_uploads: int = Field(..., description="Number of successful uploads")
+ failed_uploads: int = Field(..., description="Number of failed uploads")
+ total_items: int = Field(..., description="Total number of items processed")
+ summary: str = Field(..., description="Detailed summary")
+
+
+class LogMessage(BaseModel):
+ """Model for log messages."""
+
+ type: str = Field(..., description="Message type")
+ message: str = Field(..., description="Log message content")
+ level: str = Field(..., description="Log level")
+ logger_name: str = Field(..., description="Logger name")
+ timestamp: float = Field(..., description="Message timestamp")
+ source: str = Field(..., description="Message source")
+ auto_scroll: bool = Field(True, description="Whether to auto-scroll in UI")
+ raw_message: str = Field(..., description="Raw log message")
+ filename: str = Field("", description="Source filename")
+ line_number: int = Field(0, description="Source line number")
+
+
+class LogPollResponse(BaseModel):
+ """Response model for log polling."""
+
+ success: bool = Field(..., description="Whether polling was successful")
+ messages: List[LogMessage] = Field(..., description="List of log messages")
+ count: int = Field(..., description="Number of messages returned")
+ error: Optional[str] = Field(None, description="Error message if polling failed")
+
+
+class HealthCheckResponse(BaseModel):
+ """Response model for health check endpoint."""
+
+ status: str = Field(..., description="Service health status")
+ service: str = Field(..., description="Service name")
+
+
+class TestLoggingResponse(BaseModel):
+ """Response model for test logging endpoint."""
+
+ message: Optional[str] = Field(None, description="Success message")
+ error: Optional[str] = Field(None, description="Error message if test failed")
+
+
+class DownloadStartResponse(BaseModel):
+ """Response model for download start endpoint."""
+
+ message: str = Field(..., description="Status message")
+ synapse_id: str = Field(..., description="Synapse ID being downloaded")
+
+
+class UploadStartResponse(BaseModel):
+ """Response model for upload start endpoint."""
+
+ message: str = Field(..., description="Status message")
+ file_path: str = Field(..., description="File path being uploaded")
+
+
+class LogoutResponse(BaseModel):
+ """Response model for logout endpoint."""
+
+ message: str = Field(..., description="Logout confirmation message")
+
+
+class BulkDownloadResponse(BaseModel):
+ """Response model for bulk download operations."""
+
+ success: bool = Field(..., description="Whether the operation was successful")
+ message: str = Field(..., description="Operation summary message")
+ item_count: int = Field(..., description="Number of items processed")
+ summary: str = Field(..., description="Detailed summary of the operation")
+
+
+class CompletionMessage(BaseModel):
+ """Model for operation completion messages sent via WebSocket."""
+
+ type: str = Field("complete", description="Message type")
+ operation: str = Field(..., description="Operation that completed")
+ success: bool = Field(..., description="Whether operation was successful")
+ data: Dict[str, Any] = Field(..., description="Operation result data")
+ timestamp: Optional[float] = Field(None, description="Message timestamp")
+
+
+class ProgressMessage(BaseModel):
+ """Model for progress update messages sent via WebSocket."""
+
+ type: str = Field("progress", description="Message type")
+ operation: str = Field(..., description="Operation in progress")
+ progress: int = Field(..., description="Progress percentage (0-100)")
+ message: str = Field(..., description="Progress description")
+ timestamp: Optional[float] = Field(None, description="Message timestamp")
+
+
+class ConnectionStatusMessage(BaseModel):
+ """Model for WebSocket connection status messages."""
+
+ type: str = Field("connection_status", description="Message type")
+ connected: bool = Field(..., description="Connection status")
+ timestamp: Optional[float] = Field(None, description="Message timestamp")
+
+
+class ErrorResponse(BaseModel):
+ """Generic error response model."""
+
+ error: str = Field(..., description="Error message")
+ detail: Optional[str] = Field(None, description="Additional error details")
+ status_code: Optional[int] = Field(None, description="HTTP status code")
diff --git a/synapse-electron/backend/models/domain_models.py b/synapse-electron/backend/models/domain_models.py
new file mode 100644
index 000000000..7c73950f1
--- /dev/null
+++ b/synapse-electron/backend/models/domain_models.py
@@ -0,0 +1,93 @@
+"""
+Domain Models for Synapse Desktop Client.
+
+This module contains business domain models that represent core entities
+in the application with associated business logic methods.
+"""
+
+from dataclasses import dataclass
+from typing import Optional, Union
+
+
+@dataclass
+class BulkItem:
+ """
+ Represents an item in bulk download/upload operations.
+
+ This class holds metadata about a Synapse entity that can be
+ selected for bulk operations such as downloads or uploads.
+
+ Attributes:
+ synapse_id: Unique identifier for the Synapse entity
+ name: Display name of the item
+ item_type: Type of item ("File" or "Folder" only)
+ size: Size in bytes for files, None for folders
+ parent_id: ID of the parent container
+ path: Local or Synapse path information
+ """
+
+ synapse_id: str
+ name: str
+ item_type: str # "File" or "Folder" only
+ size: Optional[Union[int, float]] = None
+ parent_id: Optional[str] = None
+ path: Optional[str] = None
+
+ def __str__(self) -> str:
+ """
+ Return string representation of the item.
+
+ Arguments:
+ None
+
+ Returns:
+ str: Formatted string showing item type, name, and Synapse ID
+
+ Raises:
+ None: This method does not raise exceptions.
+ """
+ return f"{self.item_type}: {self.name} ({self.synapse_id})"
+
+ def is_downloadable(self) -> bool:
+ """
+ Check if this item type can be downloaded.
+
+ Determines whether the item is of a type that supports download
+ operations based on its item_type.
+
+ Arguments:
+ None
+
+ Returns:
+ bool: True if the item can be downloaded, False otherwise
+
+ Raises:
+ None: This method does not raise exceptions.
+ """
+ return self.item_type in ["File", "Folder"]
+
+ def get_display_size(self) -> str:
+ """
+ Get formatted size for display.
+
+ Converts the raw byte size into a human-readable format with
+ appropriate units (B, KB, MB, GB, TB, PB).
+
+ Arguments:
+ None
+
+ Returns:
+ str: Formatted size string with units, or empty string if no size available
+
+ Raises:
+ None: This method handles invalid sizes gracefully.
+ """
+ if not self.size or not isinstance(self.size, (int, float)):
+ return ""
+
+ size = float(self.size)
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
+ if size < 1024.0:
+ return f"{size:.1f} {unit}"
+ size /= 1024.0
+ return f"{size:.1f} PB"
diff --git a/synapse-electron/backend/server.py b/synapse-electron/backend/server.py
new file mode 100644
index 000000000..f3dbdfe59
--- /dev/null
+++ b/synapse-electron/backend/server.py
@@ -0,0 +1,1204 @@
+#!/usr/bin/env python3
+"""
+FastAPI backend server for Synapse Desktop Client.
+
+This module provides the main REST API server that bridges the existing
+Python Synapse functionality with the Electron frontend. It handles
+authentication, file operations, and bulk operations through a clean
+HTTP API interface.
+"""
+
+import argparse
+import asyncio
+import logging
+import os
+import sys
+from contextlib import asynccontextmanager
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+import uvicorn
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+
+# Add the parent directory to the path to import synapseclient modules
+current_dir = Path(__file__).parent
+parent_dir = current_dir.parent.parent
+sys.path.insert(0, str(parent_dir))
+
+try:
+ from models import ( # API Models; Domain Models
+ BulkDownloadRequest,
+ BulkItemModel,
+ BulkUploadRequest,
+ DownloadRequest,
+ EnumerateRequest,
+ LoginRequest,
+ ScanDirectoryRequest,
+ UploadRequest,
+ )
+ from services import ConfigManager, SynapseClientManager
+ from utils import (
+ broadcast_message,
+ get_home_and_downloads_directories,
+ get_queued_messages,
+ initialize_logging,
+ run_async_task_in_background,
+ scan_directory_for_files,
+ setup_electron_environment,
+ setup_logging,
+ start_websocket_server,
+ )
+except ImportError as e:
+ print(f"Error importing desktop client models: {e}")
+ print(
+ "Make sure you're running this from the correct directory and models are accessible"
+ )
+ sys.exit(1)
+
+# Configure logging with default level
+setup_logging("info")
+logger = logging.getLogger(__name__)
+
+# Global instances
+synapse_client: Optional[SynapseClientManager] = None
+config_manager: Optional[ConfigManager] = None
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI) -> Any:
+ """
+ Manage application lifespan events.
+
+ Handles startup and shutdown logic for the FastAPI application,
+ including logging initialization and cleanup.
+
+ Arguments:
+ app: The FastAPI application instance.
+
+ Returns:
+ An async context manager that handles startup and shutdown.
+
+ Raises:
+ Exception: If logging initialization fails during startup.
+ """
+ # Startup
+ setup_logging()
+ await initialize_logging()
+ yield
+ # Shutdown - add any cleanup here if needed
+
+
+def create_app() -> FastAPI:
+ """
+ Create and configure the FastAPI application.
+
+ Sets up the FastAPI application with CORS middleware and proper configuration
+ for the Synapse Desktop Client backend API.
+
+ Arguments:
+ None
+
+ Returns:
+ FastAPI: Configured FastAPI application instance with CORS middleware.
+
+ Raises:
+ Exception: If application creation or configuration fails.
+ """
+ app = FastAPI(
+ title="Synapse Desktop Client API",
+ description="Backend API for Synapse Desktop Client",
+ version="0.1.0",
+ lifespan=lifespan,
+ )
+
+ # Configure CORS for Electron app
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["http://localhost:*", "file://*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+ return app
+
+
+app = create_app()
+
+
+# API Routes - Logging
+
+
+@app.get("/logs/poll")
+async def poll_log_messages() -> Dict[str, Any]:
+ """
+ Poll for new log messages from the queue.
+
+ Retrieves queued log messages from the logging system and returns them
+ to the frontend for display in the log viewer.
+
+ Arguments:
+ None
+
+ Returns:
+ Dict[str, Any]: JSON response containing queued log messages with fields:
+ - success: Boolean indicating if polling was successful
+ - messages: List of log message dictionaries
+ - count: Number of messages returned
+ - error: Error message if polling failed (optional)
+
+ Raises:
+ Exception: If retrieving log messages fails, returns error in response.
+ """
+ try:
+ messages = get_queued_messages()
+ return {
+ "success": True,
+ "messages": [msg.model_dump() for msg in messages],
+ "count": len(messages),
+ }
+ except Exception as e:
+ logger.error("Error polling log messages: %s", e)
+ return {"success": False, "messages": [], "count": 0, "error": str(e)}
+
+
+# API Routes - Health and System
+
+
+@app.get("/health")
+async def health_check() -> Dict[str, str]:
+ """
+ Health check endpoint for service monitoring.
+
+ Provides a simple health check endpoint that can be used by monitoring
+ systems to verify that the backend service is running and responsive.
+
+ Arguments:
+ None
+
+ Returns:
+ Dict[str, str]: JSON response indicating service health status with fields:
+ - status: Health status (always "healthy" if reachable)
+ - service: Service name identifier
+
+ Raises:
+ None: This endpoint should always succeed if the service is running.
+ """
+ return {"status": "healthy", "service": "synapse-backend"}
+
+
+@app.get("/test/logging")
+async def test_logging() -> Dict[str, str]:
+ """
+ Test endpoint to verify logging functionality.
+
+ Emits test log messages at different levels to verify that the logging
+ system is working correctly and messages are being captured properly.
+
+ Arguments:
+ None
+
+ Returns:
+ Dict[str, str]: JSON response confirming logging test with fields:
+ - message: Success message if test completed
+ - error: Error message if test failed
+
+ Raises:
+ Exception: If logging test fails, returns error message in response.
+ """
+ try:
+ logger.info("Test logging endpoint called")
+ logger.debug(
+ "Test debug message - should only appear if debug level is enabled"
+ )
+ return {"message": "Test logging messages sent"}
+ except Exception as e:
+ logger.error("Test logging failed: %s", e)
+ return {"error": str(e)}
+
+
+@app.get("/system/home-directory")
+async def get_home_directory() -> Dict[str, str]:
+ """
+ Get the user's home and downloads directory paths.
+
+ Retrieves the current user's home directory and downloads directory paths,
+ creating the downloads directory if it doesn't exist.
+
+ Arguments:
+ None
+
+ Returns:
+ Dict[str, str]: JSON response with directory paths containing:
+ - home_directory: User's home directory path
+ - downloads_directory: User's downloads directory path
+
+ Raises:
+ HTTPException: If directories cannot be accessed or created.
+ """
+ try:
+ directories = get_home_and_downloads_directories()
+ return directories
+ except Exception as e:
+ logger.error("Error getting home directory: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+# API Routes - Authentication
+
+
+@app.get("/auth/profiles")
+async def get_profiles() -> Dict[str, List[Dict[str, str]]]:
+ """
+ Get available authentication profiles from configuration.
+
+ Retrieves all available Synapse authentication profiles from the user's
+ configuration file and formats them for display in the frontend.
+
+ Arguments:
+ None
+
+ Returns:
+ Dict[str, List[Dict[str, str]]]: JSON response with list of available profiles containing:
+ - profiles: List of profile dictionaries with 'name' and 'display_name' fields
+
+ Raises:
+ HTTPException: If profiles cannot be retrieved from configuration.
+ """
+ try:
+ global config_manager
+ if not config_manager:
+ config_manager = ConfigManager()
+
+ profiles = config_manager.get_available_profiles()
+ logger.info("Available profiles: %s", profiles)
+
+ profile_data = []
+ for profile in profiles:
+ profile_data.append(
+ {"name": profile, "display_name": profile.replace("_", " ").title()}
+ )
+
+ return {"profiles": profile_data}
+ except Exception as e:
+ logger.error("Error getting profiles: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+@app.post("/auth/login")
+async def login(request: LoginRequest) -> Dict[str, str]:
+ """
+ Authenticate user with Synapse.
+
+ Handles both manual authentication (with username/token) and profile-based
+ authentication using stored configuration profiles.
+
+ Arguments:
+ request: Login request containing authentication details and mode
+
+ Returns:
+ Dict[str, str]: JSON response with authentication result containing:
+ - username: Authenticated username
+ - name: Display name (same as username)
+ - user_id: User ID (empty string for compatibility)
+ - token: Authentication status token
+
+ Raises:
+ HTTPException: If authentication fails or required parameters are missing.
+ """
+ try:
+ global synapse_client, config_manager
+ if not synapse_client:
+ synapse_client = SynapseClientManager()
+ if not config_manager:
+ config_manager = ConfigManager()
+
+ result = await _perform_authentication(request)
+
+ if result["success"]:
+ return {
+ "username": result.get("username", request.username),
+ "name": result.get("username", request.username),
+ "user_id": "",
+ "token": "authenticated",
+ }
+ else:
+ raise HTTPException(status_code=401, detail=result["error"])
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error("Login error: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+async def _perform_authentication(request: LoginRequest) -> Dict[str, Any]:
+ """
+ Perform the actual authentication based on request mode.
+
+ Handles the authentication logic for both manual and profile-based login modes.
+
+ Arguments:
+ request: Login request containing authentication details and mode
+
+ Returns:
+ Dict[str, Any]: Dictionary with authentication result containing:
+ - success: Boolean indicating if authentication was successful
+ - username: Username if successful
+ - error: Error message if authentication failed
+
+ Raises:
+ HTTPException: If required parameters are missing for the authentication mode.
+ """
+ # Use default (non-debug) mode
+ debug_mode = False
+
+ if request.mode == "manual":
+ if not request.username or not request.token:
+ raise HTTPException(
+ status_code=400, detail="Username and token are required"
+ )
+ return await synapse_client.login_manual(
+ request.username, request.token, debug_mode
+ )
+ else:
+ if not request.profile:
+ raise HTTPException(status_code=400, detail="Profile is required")
+ return await synapse_client.login_with_profile(request.profile, debug_mode)
+
+
+@app.post("/auth/logout")
+async def logout() -> Dict[str, str]:
+ """
+ Logout current user.
+
+ Terminates the current Synapse session and clears authentication state
+ for the authenticated user.
+
+ Arguments:
+ None
+
+ Returns:
+ Dict[str, str]: JSON response confirming logout with:
+ - message: Confirmation message of successful logout
+
+ Raises:
+ HTTPException: If logout operation fails.
+ """
+ try:
+ global synapse_client
+ if synapse_client:
+ synapse_client.logout()
+ return {"message": "Logged out successfully"}
+ except Exception as e:
+ logger.error("Logout error: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+# API Routes - File Operations
+
+
+@app.post("/files/download")
+async def download_file(request: DownloadRequest) -> Dict[str, str]:
+ """
+ Download a file from Synapse.
+
+ Initiates a background download task for the specified Synapse entity.
+ The download runs asynchronously to avoid blocking the API response.
+
+ Arguments:
+ request: Download request with file details including synapse_id, version, and download_path
+
+ Returns:
+ Dict[str, str]: JSON response confirming download started with:
+ - message: Confirmation that download has started
+ - synapse_id: The Synapse ID being downloaded
+
+ Raises:
+ HTTPException: If user is not authenticated or download cannot be started.
+ """
+ try:
+ global synapse_client
+ if not synapse_client or not synapse_client.is_logged_in:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ async def download_task() -> None:
+ try:
+ logger.info("Starting download of %s", request.synapse_id)
+
+ result = await synapse_client.download_file(
+ synapse_id=request.synapse_id,
+ version=int(request.version) if request.version else None,
+ download_path=request.download_path,
+ progress_callback=None,
+ detail_callback=None,
+ )
+
+ if result["success"]:
+ logger.info("✅ Successfully downloaded %s", request.synapse_id)
+ logger.info(
+ "Downloaded to %s", result.get("path", "download location")
+ )
+ else:
+ logger.error("❌ Download failed: %s", result["error"])
+ except Exception as e:
+ logger.exception("Download task error: %s", e)
+ logger.error("❌ Download error: %s", str(e))
+ finally:
+ logger.info("Download task completed for %s", request.synapse_id)
+
+ run_async_task_in_background(download_task, f"download_{request.synapse_id}")
+ return {"message": "Download started", "synapse_id": request.synapse_id}
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error("Download endpoint error: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+@app.post("/files/upload")
+async def upload_file(request: UploadRequest) -> Dict[str, str]:
+ """
+ Upload a file to Synapse.
+
+ Initiates a background upload task for the specified file. Supports both
+ creating new entities and updating existing ones based on the request mode.
+
+ Arguments:
+ request: Upload request with file details including file_path, mode, parent_id/entity_id, and name
+
+ Returns:
+ Dict[str, str]: JSON response confirming upload started with:
+ - message: Confirmation that upload has started
+ - file_path: The local file path being uploaded
+
+ Raises:
+ HTTPException: If user is not authenticated or upload cannot be started.
+ """
+ try:
+ global synapse_client
+ if not synapse_client or not synapse_client.is_logged_in:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ async def upload_task() -> None:
+ try:
+ logger.info("Starting upload of %s", request.file_path)
+
+ result = await synapse_client.upload_file(
+ file_path=request.file_path,
+ parent_id=request.parent_id if request.mode == "new" else None,
+ entity_id=request.entity_id if request.mode == "update" else None,
+ name=request.name,
+ progress_callback=None,
+ detail_callback=None,
+ )
+
+ if result["success"]:
+ logger.info("✅ Successfully uploaded %s", request.file_path)
+ logger.info("Uploaded as %s", result.get("entity_id", "new entity"))
+ else:
+ logger.error("❌ Upload failed: %s", result["error"])
+ except Exception as e:
+ logger.exception("Upload task error: %s", e)
+ logger.error("❌ Upload error: %s", str(e))
+ finally:
+ logger.info("Upload task completed for %s", request.file_path)
+
+ run_async_task_in_background(
+ upload_task, f"upload_{os.path.basename(request.file_path)}"
+ )
+ return {"message": "Upload started", "file_path": request.file_path}
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error("Upload endpoint error: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+# API Routes - Bulk Operations
+
+
+@app.post("/files/scan-directory")
+async def scan_directory(request: ScanDirectoryRequest) -> Dict[str, Any]:
+ """
+ Scan a directory for files to upload.
+
+ Recursively scans the specified directory and returns file metadata
+ for all files found, including size, type, and path information.
+
+ Arguments:
+ request: Directory scan request with directory_path and recursive flag
+
+ Returns:
+ Dict[str, Any]: JSON response with file listing and summary containing:
+ - success: Boolean indicating scan success
+ - files: List of file metadata dictionaries
+ - summary: Summary statistics about scanned files
+
+ Raises:
+ HTTPException: If directory doesn't exist, is not a directory, or scanning fails.
+ """
+ try:
+ result = scan_directory_for_files(request.directory_path, request.recursive)
+ return result
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e)) from e
+ except Exception as e:
+ logger.error("Directory scan error: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+@app.post("/bulk/enumerate")
+async def enumerate_container(
+ request: EnumerateRequest,
+) -> Dict[str, List[Dict[str, Any]]]:
+ """
+ Enumerate contents of a Synapse container.
+
+ Lists all files and folders within the specified Synapse container (Project or Folder),
+ with optional recursive enumeration of subdirectories.
+
+ Arguments:
+ request: Container enumeration request with container_id and recursive flag
+
+ Returns:
+ Dict[str, List[Dict[str, Any]]]: JSON response with container contents containing:
+ - items: List of item dictionaries with metadata (id, name, type, size, etc.)
+
+ Raises:
+ HTTPException: If user is not authenticated or enumeration fails.
+ """
+ try:
+ global synapse_client
+ if not synapse_client or not synapse_client.is_logged_in:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ logger.info("Enumerating container %s", request.container_id)
+
+ result = await synapse_client.enumerate_container(
+ request.container_id, request.recursive
+ )
+
+ if result["success"]:
+ items = _convert_bulk_items_to_dict(result["items"])
+ logger.info("Found %d items in container", len(items))
+ return {"items": items}
+ else:
+ raise HTTPException(status_code=500, detail=result["error"])
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.exception("Enumerate error: %s", e)
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+def _convert_bulk_items_to_dict(items: List[Any]) -> List[Dict[str, Any]]:
+ """
+ Convert BulkItem objects to JSON-serializable dictionaries.
+
+ Transforms BulkItem objects or existing dictionaries into a standardized
+ dictionary format suitable for JSON serialization and frontend consumption.
+
+ Arguments:
+ items: List of BulkItem objects or dictionaries to convert
+
+ Returns:
+ List[Dict[str, Any]]: List of dictionaries with standardized item metadata including:
+ - id: Item Synapse ID
+ - name: Item name
+ - type: Item type (file/folder)
+ - size: File size (for files)
+ - parent_id: Parent container ID
+ - path: Item path
+
+ Raises:
+ None: Handles conversion errors gracefully by preserving existing dictionaries.
+ """
+ converted_items = []
+ for item in items:
+ if hasattr(item, "synapse_id"): # BulkItem object
+ converted_items.append(
+ {
+ "id": item.synapse_id,
+ "name": item.name,
+ "type": item.item_type.lower(),
+ "size": item.size,
+ "parent_id": item.parent_id,
+ "path": item.path if item.path else "",
+ }
+ )
+ else: # Already a dict
+ converted_items.append(item)
+ return converted_items
+
+
+@app.post("/bulk/download")
+async def bulk_download(request: BulkDownloadRequest) -> Dict[str, Any]:
+ """
+ Bulk download files from Synapse.
+
+ Downloads multiple files from Synapse to the specified local directory.
+ Only file items are processed; folders are filtered out automatically.
+
+ Arguments:
+ request: Bulk download request with items list, download_path, and options
+
+ Returns:
+ Dict[str, Any]: JSON response with download results containing:
+ - success: Boolean indicating overall operation success
+ - message: Summary message
+ - item_count: Number of items processed
+ - summary: Detailed operation summary
+
+ Raises:
+ HTTPException: If user is not authenticated, no files selected, or download fails.
+ """
+ logger.info("Bulk download endpoint")
+ try:
+ global synapse_client
+ if not synapse_client or not synapse_client.is_logged_in:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ file_items_data = _filter_file_items(request.items)
+
+ if not file_items_data:
+ raise HTTPException(
+ status_code=400,
+ detail="No files selected for download. Only files can be bulk downloaded.",
+ )
+
+ logger.info(
+ "Starting bulk download of %d files (filtered from %d selected items)",
+ len(file_items_data),
+ len(request.items),
+ )
+
+ bulk_items = _convert_dict_to_bulk_items(file_items_data)
+
+ result = await synapse_client.bulk_download(
+ items=bulk_items,
+ download_path=request.download_path,
+ recursive=request.create_subfolders,
+ progress_callback=None,
+ detail_callback=None,
+ )
+
+ return await _handle_bulk_download_result(result, file_items_data)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.exception("Bulk download endpoint error")
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+def _filter_file_items(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Filter items to only include files for bulk download.
+
+ Removes non-file items (folders, etc.) from the items list since
+ bulk download operations only support files.
+
+ Arguments:
+ items: List of item dictionaries that may contain files and folders
+
+ Returns:
+ List[Dict[str, Any]]: Filtered list containing only file items
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ return [item for item in items if item.get("type", "file").lower() == "file"]
+
+
+def _convert_dict_to_bulk_items(
+ file_items_data: List[Dict[str, Any]]
+) -> List[BulkItemModel]:
+ """
+ Convert dictionary items to BulkItem objects.
+
+ Transforms dictionary representations of file items into BulkItemModel
+ objects that can be used by the Synapse client for bulk operations.
+
+ Arguments:
+ file_items_data: List of file item dictionaries with metadata
+
+ Returns:
+ List[BulkItemModel]: List of BulkItemModel objects ready for bulk operations
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ bulk_items = []
+ for item_data in file_items_data:
+ bulk_item = BulkItemModel(
+ synapse_id=item_data["id"],
+ name=item_data["name"],
+ item_type=item_data.get("type", "file"),
+ size=item_data.get("size"),
+ parent_id=item_data.get("parent_id"),
+ path=item_data.get("path", ""),
+ )
+ bulk_items.append(bulk_item)
+ return bulk_items
+
+
+async def _handle_bulk_download_result(
+ result: Dict[str, Any], file_items_data: List[Dict[str, Any]]
+) -> Dict[str, Any]:
+ """
+ Handle the result of bulk download operation.
+
+ Processes the result from a bulk download operation and broadcasts
+ completion messages via WebSocket to notify the frontend.
+
+ Arguments:
+ result: The result dictionary from the bulk download operation
+ file_items_data: The original file items data that were downloaded
+
+ Returns:
+ Dict[str, Any]: A response dictionary with the result of the bulk download operation containing:
+ - success: Boolean indicating operation success
+ - message: Summary message
+ - item_count: Number of items processed
+ - summary: Detailed operation summary
+
+ Raises:
+ HTTPException: If the bulk download operation failed.
+ """
+ if result["success"]:
+ logger.info("Successfully downloaded %d files", len(file_items_data))
+ await broadcast_message(
+ {
+ "type": "complete",
+ "operation": "bulk-download",
+ "success": True,
+ "data": {"message": f"Downloaded {len(file_items_data)} files"},
+ }
+ )
+ return {
+ "success": True,
+ "message": "Bulk download completed successfully",
+ "item_count": len(file_items_data),
+ "summary": result.get(
+ "summary", f"Downloaded {len(file_items_data)} files"
+ ),
+ }
+ else:
+ logger.error("Bulk download failed: %s", result["error"])
+ await broadcast_message(
+ {
+ "type": "complete",
+ "operation": "bulk-download",
+ "success": False,
+ "data": {"error": result["error"]},
+ }
+ )
+ raise HTTPException(status_code=500, detail=result["error"])
+
+
+@app.post("/bulk/upload")
+async def bulk_upload(request: BulkUploadRequest) -> Dict[str, Any]:
+ """
+ Bulk upload files to Synapse with proper folder hierarchy.
+
+ Uploads multiple files to Synapse with optional preservation of the
+ local folder structure. Only file items are processed for upload.
+
+ Arguments:
+ request: Bulk upload request with parent_id, files list, and options
+
+ Returns:
+ Dict[str, Any]: JSON response with upload results containing:
+ - success: Boolean indicating overall operation success
+ - message: Summary message
+ - successful_uploads: Number of successful uploads
+ - failed_uploads: Number of failed uploads
+ - total_items: Total number of items processed
+ - summary: Detailed operation summary
+
+ Raises:
+ HTTPException: If user is not authenticated, no files selected, or upload fails.
+ """
+ try:
+ global synapse_client
+ if not synapse_client or not synapse_client.is_logged_in:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ file_items_data = _filter_file_items(request.files)
+
+ if not file_items_data:
+ raise HTTPException(
+ status_code=400,
+ detail="No files selected for upload. Only files can be bulk uploaded.",
+ )
+
+ logger.info(
+ "Starting bulk upload of %d files (filtered from %d selected items)",
+ len(file_items_data),
+ len(request.files),
+ )
+
+ result = await _perform_bulk_upload(request, file_items_data)
+ return result
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.exception("Bulk upload endpoint error")
+ logger.info("Bulk upload error: %s", str(e))
+ await broadcast_message(
+ {
+ "type": "complete",
+ "operation": "bulk-upload",
+ "success": False,
+ "data": {"error": str(e)},
+ }
+ )
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+async def _perform_bulk_upload(
+ request: BulkUploadRequest, file_items_data: List[Dict[str, Any]]
+) -> Dict[str, Any]:
+ """
+ Perform the actual bulk upload operation.
+
+ Handles the complete bulk upload process including folder hierarchy creation
+ and file uploads with proper parent-child relationships.
+
+ Arguments:
+ request: The bulk upload request with configuration options
+ file_items_data: List of filtered file items to upload
+
+ Returns:
+ Dict[str, Any]: Upload result summary with success counts and details
+
+ Raises:
+ HTTPException: If no valid files found to upload or upload preparation fails.
+ """
+
+ folder_map, root_files = _build_folder_hierarchy(request, file_items_data)
+
+ total_items = len(root_files) + len(
+ [f for f in folder_map.values() if "/" not in f.name]
+ )
+ if total_items == 0:
+ raise HTTPException(status_code=400, detail="No valid files found to upload")
+
+ logger.info(
+ "Created folder hierarchy: %d folders, %d root files",
+ len([f for f in folder_map.values() if "/" not in f.name]),
+ len(root_files),
+ )
+
+ upload_tasks = _create_upload_tasks(root_files, folder_map)
+ upload_results = await asyncio.gather(*upload_tasks, return_exceptions=True)
+
+ return _process_upload_results(upload_results, total_items, root_files, folder_map)
+
+
+def _build_folder_hierarchy(
+ request: BulkUploadRequest, file_items_data: List[Dict[str, Any]]
+) -> tuple[Dict[str, Any], List[Any]]:
+ """
+ Build folder hierarchy for preserving structure during upload.
+
+ Creates the necessary folder structure in memory before upload to preserve
+ the local directory hierarchy in Synapse when requested.
+
+ Arguments:
+ request: The bulk upload request with hierarchy preservation settings
+ file_items_data: List of file items with path information
+
+ Returns:
+ tuple[Dict[str, Any], List[Any]]: A tuple containing:
+ - folder_map: Dictionary mapping folder paths to folder objects
+ - root_files: List of files that belong at the root level
+
+ Raises:
+ None: Skips files with invalid paths and logs warnings.
+ """
+ from synapseclient.models.file import File
+
+ folder_map = {}
+ root_files = []
+
+ for file_data in file_items_data:
+ file_path = file_data.get("path")
+ relative_path = file_data.get("relative_path", "")
+
+ if not file_path or not os.path.exists(file_path):
+ logger.info(
+ "Skipping file with invalid path: %s", file_data.get("name", "Unknown")
+ )
+ continue
+
+ file_obj = File(
+ path=file_path,
+ name=file_data.get("name"),
+ description=file_data.get("description", None),
+ )
+
+ if request.preserve_folder_structure and relative_path:
+ _add_file_to_folder_hierarchy(
+ file_obj, relative_path, folder_map, request.parent_id
+ )
+ else:
+ file_obj.parent_id = request.parent_id
+ root_files.append(file_obj)
+
+ return folder_map, root_files
+
+
+def _add_file_to_folder_hierarchy(
+ file_obj: Any, relative_path: str, folder_map: Dict[str, Any], parent_id: str
+) -> None:
+ """
+ Add a file to the appropriate folder in the hierarchy.
+
+ Processes the relative path to create the necessary folder structure and
+ assigns the file to the correct parent folder.
+
+ Arguments:
+ file_obj: The file object to add to the hierarchy
+ relative_path: The relative path of the file from the upload root
+ folder_map: Dictionary tracking created folders by path
+ parent_id: The parent ID for the root of the hierarchy
+
+ Returns:
+ None: Modifies folder_map and file_obj in place
+
+ Raises:
+ None: This function handles errors gracefully.
+ """
+ from synapseclient.models.folder import Folder
+
+ relative_path = relative_path.replace("\\", "/").strip("/")
+ path_parts = relative_path.split("/")
+
+ if len(path_parts) > 1:
+ folder_parts = path_parts[:-1]
+ current_path = ""
+ current_parent_id = parent_id
+
+ for i, folder_name in enumerate(folder_parts):
+ if current_path:
+ current_path += "/"
+ current_path += folder_name
+
+ if current_path not in folder_map:
+ folder_obj = Folder(
+ name=folder_name,
+ parent_id=current_parent_id,
+ files=[],
+ folders=[],
+ )
+ folder_map[current_path] = folder_obj
+
+ if i > 0:
+ parent_path = "/".join(folder_parts[:i])
+ if parent_path in folder_map:
+ folder_map[parent_path].folders.append(folder_obj)
+
+ current_parent_id = None
+
+ folder_path = "/".join(folder_parts)
+ if folder_path in folder_map:
+ folder_map[folder_path].files.append(file_obj)
+
+
+def _create_upload_tasks(
+ root_files: List[Any], folder_map: Dict[str, Any]
+) -> List[Any]:
+ """
+ Create async upload tasks for all files and folders.
+
+ Generates a list of async tasks that can be executed concurrently to
+ upload all files and create all folders in the hierarchy.
+
+ Arguments:
+ root_files: List of files that belong at the root level
+ folder_map: Dictionary mapping folder paths to folder objects
+
+ Returns:
+ List[Any]: List of async tasks for uploading items
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ upload_tasks = []
+
+ for file_obj in root_files:
+ upload_tasks.append(_upload_item(file_obj, "file", file_obj.name))
+
+ top_level_folders = [
+ folder for path, folder in folder_map.items() if "/" not in path
+ ]
+ for folder_obj in top_level_folders:
+ upload_tasks.append(_upload_item(folder_obj, "folder", folder_obj.name))
+
+ return upload_tasks
+
+
+async def _upload_item(item: Any, item_type: str, item_name: str) -> Dict[str, Any]:
+ """
+ Upload a file or folder and return result.
+
+ Performs the actual upload of a single item (file or folder) to Synapse
+ and returns detailed result information.
+
+ Arguments:
+ item: The item object (File or Folder) to upload
+ item_type: Type of item being uploaded ("file" or "folder")
+ item_name: Display name of the item for logging
+
+ Returns:
+ Dict[str, Any]: Upload result dictionary containing:
+ - success: Boolean indicating upload success
+ - item_name: Name of the item
+ - item_type: Type of the item
+ - entity_id: Synapse ID of uploaded item (if successful)
+ - error: Error message (if failed)
+ - path: Local file path (if applicable)
+
+ Raises:
+ Exception: Catches and handles upload errors, returning them in result dict.
+ """
+ try:
+ logger.info("Uploading %s: %s", item_type, item_name)
+
+ stored_item = await item.store_async(synapse_client=synapse_client.client)
+
+ return {
+ "success": True,
+ "item_name": item_name,
+ "item_type": item_type,
+ "entity_id": stored_item.id,
+ "path": getattr(item, "path", None),
+ }
+ except Exception as e:
+ logger.error("Failed to upload %s: %s", item_name, str(e))
+ return {
+ "success": False,
+ "item_name": item_name,
+ "item_type": item_type,
+ "error": str(e),
+ "path": getattr(item, "path", None),
+ }
+
+
+def _process_upload_results(
+ upload_results: List[Any],
+ total_items: int,
+ root_files: List[Any],
+ folder_map: Dict[str, Any],
+) -> Dict[str, Any]:
+ """
+ Process upload results and return final response.
+
+ Analyzes the results from all upload tasks and compiles a summary
+ response with success/failure counts and details.
+
+ Arguments:
+ upload_results: List of results from upload tasks (may include exceptions)
+ total_items: Total number of items that were attempted to upload
+ root_files: List of root-level files that were uploaded
+ folder_map: Dictionary of folders that were created
+
+ Returns:
+ Dict[str, Any]: Final upload summary containing:
+ - success: Always True (individual failures are counted)
+ - message: Summary message
+ - successful_uploads: Number of successful uploads
+ - failed_uploads: Number of failed uploads
+ - total_items: Total number of items processed
+ - summary: Detailed summary string
+
+ Raises:
+ None: This function processes exceptions and includes them in results.
+ """
+ processed_results = []
+ for i, result in enumerate(upload_results):
+ if isinstance(result, Exception):
+ item_name = "Unknown"
+ if i < len(root_files):
+ item_name = root_files[i].name
+ elif i - len(root_files) < len(
+ [f for f in folder_map.values() if "/" not in f.name]
+ ):
+ top_level_folders = [
+ f for f in folder_map.values() if "/" not in f.name
+ ]
+ item_name = top_level_folders[i - len(root_files)].name
+
+ processed_results.append(
+ {"success": False, "item_name": item_name, "error": str(result)}
+ )
+ else:
+ processed_results.append(result)
+
+ successful_uploads = sum(1 for r in processed_results if r.get("success", False))
+ failed_uploads = total_items - successful_uploads
+
+ logger.info(
+ "Bulk upload completed: %d successful, %d failed",
+ successful_uploads,
+ failed_uploads,
+ )
+
+ return {
+ "success": True,
+ "message": "Bulk upload completed successfully",
+ "successful_uploads": successful_uploads,
+ "failed_uploads": failed_uploads,
+ "total_items": total_items,
+ "summary": f"Uploaded {successful_uploads} items, {failed_uploads} failed",
+ }
+
+
+def main() -> None:
+ """
+ Main entry point for the backend server.
+
+ Parses command line arguments and starts the FastAPI server with WebSocket
+ support and appropriate configuration for the Synapse Desktop Client environment.
+
+ Arguments:
+ None: Uses command line arguments via argparse
+
+ Returns:
+ None: Runs the server until interrupted
+
+ Raises:
+ SystemExit: If argument parsing fails or server startup fails.
+ """
+ parser = argparse.ArgumentParser(description="Synapse Desktop Client Backend")
+ parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
+ parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
+ parser.add_argument("--ws-port", type=int, default=8001, help="WebSocket port")
+ parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
+
+ args = parser.parse_args()
+
+ # Setup environment for proper operation when launched from Electron
+ setup_electron_environment()
+
+ # Start WebSocket server
+ start_websocket_server(args.ws_port)
+
+ # Start FastAPI server
+ logger.info("Starting Synapse Backend Server on %s:%s", args.host, args.port)
+ logger.info("WebSocket server on ws://%s:%s", args.host, args.ws_port)
+
+ uvicorn.run(
+ app, host=args.host, port=args.port, reload=args.reload, log_level="info"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/synapse-electron/backend/services/__init__.py b/synapse-electron/backend/services/__init__.py
new file mode 100644
index 000000000..de4f93774
--- /dev/null
+++ b/synapse-electron/backend/services/__init__.py
@@ -0,0 +1,14 @@
+"""
+Services package for Synapse Desktop Client backend.
+
+This package contains service classes that handle business logic
+and external service interactions.
+"""
+
+from .config_service import ConfigManager
+from .synapse_service import SynapseClientManager
+
+__all__ = [
+ "ConfigManager",
+ "SynapseClientManager",
+]
diff --git a/synapse-electron/backend/services/config_service.py b/synapse-electron/backend/services/config_service.py
new file mode 100644
index 000000000..e35d8dddd
--- /dev/null
+++ b/synapse-electron/backend/services/config_service.py
@@ -0,0 +1,136 @@
+"""
+Configuration service for Synapse Desktop Client.
+
+This module provides the ConfigManager service class for managing
+Synapse configuration profiles and authentication settings.
+"""
+import os
+from typing import List, Optional
+
+from synapseclient.api.configuration_services import get_config_file
+
+
+class ConfigManager:
+ """
+ Manages Synapse configuration profiles.
+
+ Handles reading and parsing Synapse configuration files to extract
+ available authentication profiles and their associated information.
+ Provides methods to access profile data and validate configuration files.
+ """
+
+ def __init__(self, config_path: Optional[str] = None) -> None:
+ """
+ Initialize the configuration manager.
+
+ Sets up the configuration manager with the specified or default
+ Synapse configuration file path.
+
+ Arguments:
+ config_path: Path to the Synapse configuration file.
+ Defaults to ~/.synapseConfig if not provided.
+
+ Returns:
+ None
+
+ Raises:
+ None: Initialization does not perform file operations that could fail.
+ """
+ self.config_path = config_path or os.path.expanduser("~/.synapseConfig")
+
+ def get_available_profiles(self) -> List[str]:
+ """
+ Get list of available authentication profiles from configuration file.
+
+ Parses the Synapse configuration file to extract all available
+ authentication profiles including default, named profiles, and legacy.
+
+ Arguments:
+ None
+
+ Returns:
+ List[str]: List of profile names found in the configuration file.
+ Returns empty list if no profiles found or file cannot be read.
+
+ Raises:
+ Exception: Catches and handles configuration file parsing errors,
+ returning empty list on failure.
+ """
+ profiles = []
+
+ try:
+ config = get_config_file(self.config_path)
+ sections = config.sections()
+
+ for section in sections:
+ if section == "default":
+ profiles.append("default")
+ elif section.startswith("profile "):
+ profile_name = section[8:]
+ profiles.append(profile_name)
+ elif section == "authentication":
+ profiles.append("authentication (legacy)")
+
+ if not profiles and os.path.exists(self.config_path):
+ profiles.append("default")
+
+ except Exception:
+ pass
+
+ return profiles
+
+ def get_profile_info(self, profile_name: str) -> str:
+ """
+ Get username for a specific profile.
+
+ Retrieves the username associated with the specified profile from
+ the configuration file.
+
+ Arguments:
+ profile_name: Name of the profile to get information for
+
+ Returns:
+ str: Username associated with the profile, or empty string if not found
+
+ Raises:
+ Exception: Catches and handles configuration file access errors,
+ returning empty string on failure.
+ """
+ try:
+ config = get_config_file(self.config_path)
+
+ if profile_name == "default":
+ section_name = "default"
+ elif profile_name == "authentication (legacy)":
+ section_name = "authentication"
+ else:
+ section_name = f"profile {profile_name}"
+
+ if config.has_section(section_name):
+ username = config.get(section_name, "username", fallback="")
+ return username
+
+ except Exception:
+ pass
+
+ return ""
+
+ def has_config_file(self) -> bool:
+ """
+ Check if configuration file exists and contains profiles.
+
+ Validates that the configuration file exists on the filesystem and
+ contains at least one authentication profile.
+
+ Arguments:
+ None
+
+ Returns:
+ bool: True if config file exists and has profiles, False otherwise
+
+ Raises:
+ None: This method handles all exceptions internally.
+ """
+ return (
+ os.path.exists(self.config_path) and len(self.get_available_profiles()) > 0
+ )
diff --git a/synapse-electron/backend/services/synapse_service.py b/synapse-electron/backend/services/synapse_service.py
new file mode 100644
index 000000000..c5d36eab8
--- /dev/null
+++ b/synapse-electron/backend/services/synapse_service.py
@@ -0,0 +1,963 @@
+"""
+Synapse service for Synapse Desktop Client.
+
+This module provides the SynapseClientManager service class and supporting utilities
+for handling all Synapse client operations, including authentication, file
+uploads/downloads, and bulk operations, separated from UI logic.
+"""
+import asyncio
+import io
+import logging
+import os
+import re
+import sys
+from pathlib import Path
+from typing import Any, Callable, Dict, Optional, Tuple
+
+import synapseclient
+from synapseclient.models import File
+
+DESKTOP_CLIENT_VERSION = "0.1.0"
+
+# Set up logger for this module
+logger = logging.getLogger(__name__)
+
+
+def _safe_stderr_redirect(new_stderr: Any) -> Tuple[Any, Any]:
+ """
+ Safely redirect stderr, handling the case where original stderr might be None.
+
+ Provides a safe way to redirect stderr for capturing TQDM output while
+ ensuring we can always restore to a valid stderr stream.
+
+ Arguments:
+ new_stderr: New stderr object to redirect to
+
+ Returns:
+ Tuple[Any, Any]: Tuple of (original_stderr, safe_original_stderr) where
+ safe_original_stderr is guaranteed to be a valid file-like object
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ original_stderr = sys.stderr
+ safe_original_stderr = (
+ original_stderr if original_stderr is not None else io.StringIO()
+ )
+ sys.stderr = new_stderr
+ return original_stderr, safe_original_stderr
+
+
+def _safe_stderr_restore(original_stderr: Any, safe_original_stderr: Any) -> None:
+ """
+ Safely restore stderr, ensuring we never set it to None.
+
+ Restores stderr to its original state while ensuring we never set
+ sys.stderr to None which could cause issues.
+
+ Arguments:
+ original_stderr: The original stderr object (may be None)
+ safe_original_stderr: Fallback stderr object if original was None
+
+ Returns:
+ None
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ if original_stderr is not None:
+ sys.stderr = original_stderr
+ else:
+ sys.stderr = safe_original_stderr
+
+
+class TQDMProgressCapture:
+ """
+ Capture TQDM progress updates for GUI display.
+
+ Intercepts TQDM progress output and extracts progress information
+ to provide callbacks for GUI progress bars and status updates.
+ """
+
+ def __init__(
+ self,
+ progress_callback: Optional[Callable[[int, str], None]],
+ detail_callback: Optional[Callable[[str], None]],
+ ) -> None:
+ """
+ Initialize the TQDM progress capture.
+
+ Sets up progress capture with optional callbacks for progress updates
+ and detailed progress messages.
+
+ Arguments:
+ progress_callback: Function to call with progress updates (progress%, message) or None
+ detail_callback: Function to call with detailed progress messages or None
+
+ Returns:
+ None
+
+ Raises:
+ None: Initialization does not perform operations that could fail.
+ """
+ self.progress_callback = progress_callback
+ self.detail_callback = detail_callback
+ self.last_progress = 0
+
+ def write(self, s: str) -> None:
+ """
+ Capture TQDM output and extract progress information.
+
+ Parses TQDM output strings to extract progress percentages and
+ detailed progress information for callback notifications.
+
+ Arguments:
+ s: String output from TQDM to parse for progress information
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Catches and handles progress parsing errors gracefully.
+ """
+ if s and "\r" in s:
+ # TQDM typically uses \r for progress updates
+ progress_line = s.strip().replace("\r", "")
+ if "%" in progress_line and (
+ "B/s" in progress_line or "it/s" in progress_line
+ ):
+ try:
+ # Look for percentage in the format "XX%"
+ match = re.search(r"(\d+)%", progress_line)
+ if match:
+ progress = int(match.group(1))
+ if progress != self.last_progress:
+ self.last_progress = progress
+ if self.progress_callback:
+ self.progress_callback(
+ progress, f"Progress: {progress}%"
+ )
+ if self.detail_callback:
+ self.detail_callback(progress_line)
+ except Exception:
+ pass
+
+ def flush(self) -> None:
+ """
+ Required for file-like object interface.
+
+ Provides the flush method required for file-like objects,
+ though no actual flushing is needed for this implementation.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ None: This method does not raise exceptions.
+ """
+ pass
+
+
+class SynapseClientManager:
+ """
+ Handles all Synapse client operations.
+
+ Manages authentication, file operations, and bulk operations for the
+ Synapse platform, providing a clean interface between the GUI and
+ the underlying synapseclient library.
+ """
+
+ def __init__(self) -> None:
+ """
+ Initialize the Synapse client manager with default state.
+
+ Sets up the client manager with initial state values for
+ authentication and debugging configuration.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ None: Initialization does not perform operations that could fail.
+ """
+ self.client: Optional[synapseclient.Synapse] = None
+ self.is_logged_in = False
+ self.username = ""
+ self.debug_mode = False
+
+ def _create_synapse_client(self, debug_mode: bool = False) -> synapseclient.Synapse:
+ """
+ Create a new Synapse client instance with the specified debug mode.
+
+ Creates and configures a Synapse client with appropriate settings
+ for the desktop client environment.
+
+ Arguments:
+ debug_mode: Whether to enable debug mode for the client
+
+ Returns:
+ synapseclient.Synapse: New Synapse client instance with desktop client configuration
+
+ Raises:
+ Exception: If client creation fails (propagated from synapseclient).
+ """
+ return synapseclient.Synapse(
+ skip_checks=True,
+ debug=debug_mode,
+ user_agent=[f"synapsedesktopclient/{DESKTOP_CLIENT_VERSION}"],
+ silent_progress_bars=True,
+ )
+
+ async def login_manual(
+ self, username: str, token: str, debug_mode: bool = False
+ ) -> Dict[str, Any]:
+ """
+ Login with username and authentication token.
+
+ Performs manual authentication using username and personal access token,
+ setting up the client for subsequent operations.
+
+ Arguments:
+ username: Synapse username or email address
+ token: Personal access token for authentication
+ debug_mode: Whether to enable debug mode for the Synapse client
+
+ Returns:
+ Dict[str, Any]: Dictionary with 'success' boolean and either 'username' or 'error' key
+
+ Raises:
+ Exception: Authentication errors are caught and returned in the result dictionary.
+ """
+ try:
+ self.debug_mode = debug_mode
+ self.client = self._create_synapse_client(debug_mode)
+
+ if username:
+ self.client.login(email=username, authToken=token, silent=False)
+ else:
+ self.client.login(authToken=token, silent=False)
+
+ self.is_logged_in = True
+ self.username = getattr(self.client, "username", None) or getattr(
+ self.client, "email", "Unknown User"
+ )
+
+ logger.info(f"Successfully logged in as {self.username}")
+
+ return {"success": True, "username": self.username}
+ except Exception:
+ logger.exception("Login failed")
+ return {"success": False, "error": "Authentication failed"}
+
+ async def login_with_profile(
+ self, profile_name: str, debug_mode: bool = False
+ ) -> Dict[str, Any]:
+ """
+ Login with configuration file profile.
+
+ Performs authentication using a named profile from the Synapse
+ configuration file (~/.synapseConfig).
+
+ Arguments:
+ profile_name: Name of the profile from Synapse configuration file
+ debug_mode: Whether to enable debug mode for the Synapse client
+
+ Returns:
+ Dict[str, Any]: Dictionary with 'success' boolean and either 'username' or 'error' key
+
+ Raises:
+ Exception: Authentication errors are caught and returned in the result dictionary.
+ """
+ try:
+ self.debug_mode = debug_mode
+ self.client = self._create_synapse_client(debug_mode)
+
+ if profile_name == "authentication (legacy)":
+ self.client.login(silent=False)
+ else:
+ self.client.login(profile=profile_name, silent=False)
+
+ self.is_logged_in = True
+ self.username = getattr(self.client, "username", None) or getattr(
+ self.client, "email", "Unknown User"
+ )
+
+ logger.info(
+ f"Successfully logged in as {self.username} using profile {profile_name}"
+ )
+
+ return {"success": True, "username": self.username}
+ except Exception:
+ logger.exception("Login with profile '{profile_name}' failed")
+ return {"success": False, "error": "Authentication failed"}
+
+ def logout(self) -> None:
+ """
+ Logout from Synapse and clear authentication state.
+
+ Terminates the current Synapse session and resets all authentication
+ state to prepare for a new login.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Logout errors are logged but not propagated.
+ """
+ if self.client:
+ self.client.logout()
+ logger.info(f"User {self.username} logged out")
+ self.client = None
+ self.is_logged_in = False
+ self.username = ""
+
+ async def download_file(
+ self,
+ synapse_id: str,
+ version: Optional[int],
+ download_path: str,
+ progress_callback: Optional[Callable[[int, str], None]],
+ detail_callback: Optional[Callable[[str], None]],
+ ) -> Dict[str, Any]:
+ """
+ Download a file from Synapse.
+
+ Downloads a specific file or version from Synapse to the local filesystem,
+ with optional progress tracking through callbacks.
+
+ Arguments:
+ synapse_id: Synapse entity ID to download
+ version: Specific version to download (None for latest)
+ download_path: Local directory path for download
+ progress_callback: Function for progress updates (progress%, message)
+ detail_callback: Function for detailed progress messages
+
+ Returns:
+ Dict[str, Any]: Dictionary with 'success' boolean and either 'path' or 'error' key
+
+ Raises:
+ Exception: Download errors are caught and returned in the result dictionary.
+ """
+ try:
+ if not self.is_logged_in:
+ return {"success": False, "error": "Not logged in"}
+
+ version_info = f" version {version}" if version else ""
+ logger.info(
+ f"Starting download of {synapse_id}{version_info} to {download_path}"
+ )
+
+ progress_capture = TQDMProgressCapture(progress_callback, detail_callback)
+ original_stderr, safe_original_stderr = _safe_stderr_redirect(
+ progress_capture
+ )
+
+ try:
+ file_obj = File(
+ id=synapse_id,
+ version_number=version,
+ path=download_path,
+ download_file=True,
+ )
+
+ file_obj = file_obj.get(synapse_client=self.client)
+
+ if file_obj.path and os.path.exists(file_obj.path):
+ logger.info(
+ f"Successfully downloaded {synapse_id} to {file_obj.path}"
+ )
+ return {"success": True, "path": file_obj.path}
+ else:
+ error_msg = f"No files associated with entity {synapse_id}"
+ logger.error(error_msg)
+ return {"success": False, "error": error_msg}
+ finally:
+ _safe_stderr_restore(original_stderr, safe_original_stderr)
+
+ except Exception as e:
+ logger.exception(f"Download failed for {synapse_id}")
+ return {"success": False, "error": str(e)}
+
+ async def upload_file(
+ self,
+ file_path: str,
+ parent_id: Optional[str],
+ entity_id: Optional[str],
+ name: Optional[str],
+ progress_callback: Optional[Callable[[int, str], None]],
+ detail_callback: Optional[Callable[[str], None]],
+ ) -> Dict[str, Any]:
+ """
+ Upload a file to Synapse.
+
+ Args:
+ file_path: Local path to the file to upload
+ parent_id: Parent entity ID for new files (required for new files)
+ entity_id: Entity ID to update (for updating existing files)
+ name: Name for the entity (optional, uses filename if not provided)
+ progress_callback: Function for progress updates (progress%, message) or None
+ detail_callback: Function for detailed progress messages or None
+
+ Returns:
+ Dictionary with 'success' boolean and either entity info or 'error' key
+ """
+ try:
+ if not self.is_logged_in:
+ return {"success": False, "error": "Not logged in"}
+
+ if not os.path.exists(file_path):
+ return {"success": False, "error": f"File does not exist: {file_path}"}
+
+ if entity_id:
+ logger.info(
+ f"Starting upload of {file_path} to update entity {entity_id}"
+ )
+ else:
+ logger.info(
+ f"Starting upload of {file_path} to create new entity in {parent_id}"
+ )
+
+ progress_capture = TQDMProgressCapture(progress_callback, detail_callback)
+ original_stderr, safe_original_stderr = _safe_stderr_redirect(
+ progress_capture
+ )
+
+ try:
+ if entity_id:
+ file_obj = File(
+ id=entity_id, path=file_path, name=name, download_file=False
+ )
+ file_obj = await file_obj.get_async(synapse_client=self.client)
+ file_obj.path = file_path
+ if name:
+ file_obj.name = name
+ else:
+ if not parent_id:
+ return {
+ "success": False,
+ "error": "Parent ID is required for new files",
+ }
+
+ file_obj = File(
+ path=file_path,
+ name=name or None,
+ parent_id=parent_id,
+ )
+
+ file_obj = await file_obj.store_async(synapse_client=self.client)
+
+ logger.info(
+ f"Successfully uploaded {file_path} as entity {file_obj.id}: {file_obj.name}"
+ )
+
+ return {
+ "success": True,
+ "entity_id": file_obj.id,
+ "name": file_obj.name,
+ }
+ finally:
+ _safe_stderr_restore(original_stderr, safe_original_stderr)
+
+ except Exception as e:
+ logger.exception(f"Upload failed for {file_path}")
+ return {"success": False, "error": str(e)}
+
+ async def enumerate_container(
+ self, container_id: str, recursive: bool
+ ) -> Dict[str, Any]:
+ """
+ Enumerate contents of a Synapse container (Project or Folder).
+
+ Args:
+ container_id: Synapse ID of the container to enumerate
+ recursive: Whether to enumerate recursively
+
+ Returns:
+ Dictionary with success status and list of BulkItem objects
+ """
+ try:
+ if not self.client:
+ return {"success": False, "error": "Not logged in"}
+
+ # Log enumeration start
+ recursive_info = " (recursive)" if recursive else ""
+ logger.info(
+ f"Starting enumeration of container {container_id}{recursive_info}"
+ )
+
+ # Import here to avoid circular imports
+ from synapseclient.models import Folder
+
+ container = Folder(id=container_id)
+
+ # Sync metadata only (download_file=False)
+ # Note: Progress bars are automatically disabled in packaged environments
+ await container.sync_from_synapse_async(
+ download_file=False,
+ recursive=recursive,
+ include_types=["file", "folder"],
+ synapse_client=self.client,
+ )
+
+ items = self._convert_to_bulk_items(
+ container=container, recursive=recursive
+ )
+
+ # Build hierarchical paths for all items
+ try:
+ path_mapping = self._build_hierarchical_paths(items, container_id)
+ except Exception:
+ logger.exception("Error building hierarchical paths")
+ raise
+
+ # Update items with correct hierarchical paths
+ for i, item in enumerate(items):
+ if item.synapse_id in path_mapping:
+ item.path = path_mapping[item.synapse_id]
+
+ logger.info(
+ f"Successfully enumerated {len(items)} items from container {container_id}"
+ )
+
+ return {"success": True, "items": items}
+
+ except Exception as e:
+ logger.exception(f"Enumeration failed for container {container_id}")
+
+ return {"success": False, "error": str(e)}
+
+ def _build_hierarchical_paths(self, items: list, root_container_id: str) -> dict:
+ """
+ Build hierarchical paths for items based on parent-child relationships.
+
+ Args:
+ items: List of BulkItem objects
+ root_container_id: ID of the root container being enumerated
+
+ Returns:
+ Dictionary mapping synapse_id to hierarchical path
+ """
+ # Create a mapping of ID to item for quick lookups
+ id_to_item = {item.synapse_id: item for item in items}
+
+ # Add the root container to avoid issues
+ id_to_item[root_container_id] = None
+
+ # Function to recursively build path for an item
+ def get_item_path(item_id: str, visited: set = None) -> str:
+ if visited is None:
+ visited = set()
+
+ if item_id in visited:
+ # Circular reference protection
+ return ""
+
+ if item_id == root_container_id or item_id not in id_to_item:
+ return ""
+
+ visited.add(item_id)
+ item = id_to_item[item_id]
+
+ if (
+ item is None
+ or item.parent_id is None
+ or item.parent_id == root_container_id
+ ):
+ visited.remove(item_id)
+ return item.name if item else ""
+
+ parent_path = get_item_path(item.parent_id, visited)
+ visited.remove(item_id)
+
+ if parent_path:
+ return f"{parent_path}/{item.name}"
+ else:
+ return item.name
+
+ # Build paths for all items
+ path_mapping = {}
+ for item in items:
+ if item.item_type.lower() == "file":
+ # For files, we want the directory path (excluding the filename)
+ parent_path = ""
+ if item.parent_id and item.parent_id != root_container_id:
+ parent_path = get_item_path(item.parent_id)
+ path_mapping[item.synapse_id] = parent_path
+ else:
+ # For folders, include the folder name in the path
+ path_mapping[item.synapse_id] = get_item_path(item.synapse_id)
+
+ return path_mapping
+
+ def _convert_to_bulk_items(self, container: Any, recursive: bool) -> list:
+ """
+ Convert container contents to BulkItem objects.
+
+ Args:
+ container: Container object with populated files/folders
+ recursive: Whether enumeration was recursive
+
+ Returns:
+ List of BulkItem objects
+ """
+ from models.domain_models import BulkItem
+
+ items = []
+
+ if hasattr(container, "files"):
+ for file in container.files:
+ file_path = file.path if hasattr(file, "path") else None
+
+ items.append(
+ BulkItem(
+ synapse_id=file.id,
+ name=file.name,
+ item_type="File",
+ size=file.file_handle.content_size if file.file_handle else 0,
+ parent_id=file.parent_id,
+ path=file_path,
+ )
+ )
+
+ if hasattr(container, "folders"):
+ for folder in container.folders:
+ folder_path = folder.path if hasattr(folder, "path") else None
+
+ items.append(
+ BulkItem(
+ synapse_id=folder.id,
+ name=folder.name,
+ item_type="Folder",
+ size=None,
+ parent_id=folder.parent_id,
+ path=folder_path,
+ )
+ )
+
+ if recursive:
+ items.extend(self._convert_to_bulk_items(folder, recursive))
+
+ return items
+
+ async def _safe_callback(self, callback, *args):
+ """Safely call a callback function, handling both sync and async callbacks."""
+ if callback is None:
+ return
+ try:
+ # Check if the callback is a coroutine function (async)
+ if asyncio.iscoroutinefunction(callback):
+ await callback(*args)
+ else:
+ # Call synchronous callback
+ callback(*args)
+ except Exception as e:
+ logger.warning(f"Callback error: {e}")
+
+ async def bulk_download(
+ self,
+ items: list,
+ download_path: str,
+ recursive: bool,
+ progress_callback: Callable[[int, str], None],
+ detail_callback: Callable[[str], None],
+ ) -> Dict[str, Any]:
+ """
+ Download multiple items from Synapse.
+
+ Args:
+ items: List of BulkItem objects to download
+ download_path: Base directory for downloads
+ recursive: Whether to download folders recursively
+ progress_callback: Callback for progress updates
+ detail_callback: Callback for detailed progress messages
+
+ Returns:
+ Dictionary with success status and results
+ """
+ try:
+ if not self.client:
+ return {"success": False, "error": "Not logged in"}
+
+ results = []
+ total_items = len(items)
+
+ for i, item in enumerate(items):
+ overall_progress = int((i / total_items) * 100)
+ await self._safe_callback(
+ progress_callback,
+ overall_progress,
+ f"Processing item {i + 1} of {total_items}",
+ )
+ await self._safe_callback(
+ detail_callback, f"Downloading {item.name} ({item.synapse_id})"
+ )
+
+ try:
+ if item.item_type.lower() == "file":
+ # Determine the download path, considering the item's path within the container
+ item_download_path = download_path
+ if recursive and item.path and item.path.strip():
+ item_download_path = os.path.join(download_path, item.path)
+
+ result = await self.download_file(
+ synapse_id=item.synapse_id,
+ version=None,
+ download_path=item_download_path,
+ progress_callback=progress_callback,
+ detail_callback=detail_callback,
+ )
+ results.append({"item": item, "result": result})
+ except Exception as e:
+ results.append(
+ {"item": item, "result": {"success": False, "error": str(e)}}
+ )
+
+ await self._safe_callback(progress_callback, 100, "Bulk download complete")
+
+ successes = sum(1 for r in results if r["result"].get("success", False))
+ failures = total_items - successes
+
+ return {
+ "success": True,
+ "results": results,
+ "summary": f"Downloaded {successes} items, {failures} failed",
+ }
+
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ async def bulk_upload(
+ self,
+ items: list,
+ parent_id: str,
+ preserve_structure: bool,
+ progress_callback: Callable[[int, str], None],
+ detail_callback: Callable[[str], None],
+ ) -> Dict[str, Any]:
+ """
+ Upload multiple items to Synapse.
+
+ Args:
+ items: List of BulkItem objects to upload
+ parent_id: Parent folder ID in Synapse
+ preserve_structure: Whether to preserve directory structure
+ progress_callback: Callback for progress updates
+ detail_callback: Callback for detailed progress messages
+
+ Returns:
+ Dictionary with success status and results
+ """
+ try:
+ if not self.client:
+ return {"success": False, "error": "Not logged in"}
+
+ results = []
+
+ # If preserving structure, we need to create folders first
+ folder_mapping = {}
+ if preserve_structure:
+ folder_mapping = await self._create_folder_structure(
+ items=items,
+ base_parent_id=parent_id,
+ progress_callback=progress_callback,
+ detail_callback=detail_callback,
+ )
+
+ file_items = [item for item in items if item.item_type == "File"]
+
+ for i, item in enumerate(file_items):
+ overall_progress = int((i / len(file_items)) * 100)
+ await self._safe_callback(
+ progress_callback,
+ overall_progress,
+ f"Uploading file {i + 1} of {len(file_items)}",
+ )
+ await self._safe_callback(detail_callback, f"Uploading {item.name}")
+
+ try:
+ target_parent = parent_id
+ if preserve_structure and item.path:
+ # Find the appropriate parent folder for this file
+ item_dir = os.path.dirname(item.path)
+
+ normalized_item_dir = item_dir.replace("\\", "/")
+
+ if normalized_item_dir in folder_mapping:
+ target_parent = folder_mapping[normalized_item_dir]
+ else:
+ # Check if this file belongs to any created folder
+ # Find the deepest (most specific) folder that contains this file
+ best_match = ""
+ for folder_path, folder_id in folder_mapping.items():
+ if self._is_subpath(item.path, folder_path):
+ if len(folder_path) > len(best_match):
+ best_match = folder_path
+ target_parent = folder_id
+
+ result = await self.upload_file(
+ file_path=item.path,
+ parent_id=target_parent,
+ entity_id=None,
+ name=item.name,
+ progress_callback=progress_callback,
+ detail_callback=detail_callback,
+ )
+
+ results.append({"item": item, "result": result})
+
+ except Exception as e:
+ results.append(
+ {"item": item, "result": {"success": False, "error": str(e)}}
+ )
+
+ await self._safe_callback(progress_callback, 100, "Bulk upload complete")
+
+ successes = sum(1 for r in results if r["result"].get("success", False))
+ failures = len(file_items) - successes
+
+ return {
+ "success": True,
+ "results": results,
+ "summary": f"Uploaded {successes} files, {failures} failed",
+ }
+
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ async def _create_folder_structure(
+ self,
+ items: list,
+ base_parent_id: str,
+ progress_callback: Callable[[int, str], None],
+ detail_callback: Callable[[str], None],
+ ) -> Dict[str, str]:
+ """
+ Create folder structure in Synapse for bulk upload.
+
+ Args:
+ items: List of BulkItem objects
+ base_parent_id: Base parent folder ID
+ progress_callback: Callback for progress updates
+ detail_callback: Callback for detailed progress messages
+
+ Returns:
+ Dictionary mapping local paths to Synapse folder IDs
+ """
+ from synapseclient.models import Folder
+
+ folder_mapping = {}
+
+ root_folders = []
+ for item in items:
+ if item.item_type == "Folder":
+ is_root = True
+ for other_item in items:
+ if (
+ other_item.item_type == "Folder"
+ and other_item.path != item.path
+ and self._is_subpath(item.path, other_item.path)
+ ):
+ is_root = False
+ break
+ if is_root:
+ root_folders.append(item)
+
+ dir_paths = set()
+
+ # First, add the explicitly selected folders
+ for root_folder in root_folders:
+ dir_paths.add(root_folder.path)
+
+ # Then, add subdirectories within selected folders only
+ for item in items:
+ if item.path and item.item_type == "File":
+ dir_path = os.path.dirname(item.path)
+ if dir_path:
+ # Only add directories that are within a selected root folder
+ for root_folder in root_folders:
+ if self._is_subpath(dir_path, root_folder.path):
+ # Add all directories between root folder and file's directory
+ current_path = Path(dir_path)
+ root_path = Path(root_folder.path)
+
+ # Build list of directories from root folder to file's directory
+ relative_parts = current_path.relative_to(root_path).parts
+ temp_path = root_path
+
+ for part in relative_parts:
+ temp_path = temp_path / part
+ dir_paths.add(str(temp_path))
+ break
+
+ sorted_dirs = sorted(dir_paths, key=lambda x: len(Path(x).parts))
+
+ await detail_callback("Creating folder structure...")
+
+ for i, dir_path in enumerate(sorted_dirs):
+ if i % 5 == 0:
+ progress = int((i / len(sorted_dirs)) * 50)
+ await progress_callback(
+ progress, f"Creating folders ({i}/{len(sorted_dirs)})"
+ )
+
+ path_obj = Path(dir_path)
+ folder_name = path_obj.name
+
+ parent_folder_id = base_parent_id
+
+ # For root folders, parent is the base parent
+ is_root_folder = any(
+ root_folder.path == dir_path for root_folder in root_folders
+ )
+
+ if not is_root_folder:
+ # Find the parent directory that should already be created
+ parent_path = str(path_obj.parent)
+ normalized_parent_path = parent_path.replace("\\", "/")
+ if normalized_parent_path in folder_mapping:
+ parent_folder_id = folder_mapping[normalized_parent_path]
+ else:
+ # Parent might be a root folder
+ for root_folder in root_folders:
+ if root_folder.path == parent_path:
+ parent_folder_id = folder_mapping[normalized_parent_path]
+ break
+
+ try:
+ folder = Folder(name=folder_name, parent_id=parent_folder_id)
+ folder = folder.store(synapse_client=self.client)
+ normalized_dir_path = dir_path.replace("\\", "/")
+ folder_mapping[normalized_dir_path] = folder.id
+ await detail_callback(
+ f"Created folder: {folder_name} ({folder.id}) from path {dir_path}"
+ )
+ except Exception as e:
+ await detail_callback(f"Error creating folder {folder_name}: {str(e)}")
+
+ return folder_mapping
+
+ def _is_subpath(self, child_path: str, parent_path: str) -> bool:
+ """
+ Check if child_path is a subpath of parent_path.
+
+ Args:
+ child_path: The potential child path to check
+ parent_path: The potential parent path
+
+ Returns:
+ True if child_path is a subpath of parent_path, False otherwise
+ """
+ try:
+ Path(child_path).relative_to(Path(parent_path))
+ return True
+ except ValueError:
+ return False
diff --git a/synapse-electron/backend/utils/__init__.py b/synapse-electron/backend/utils/__init__.py
new file mode 100644
index 000000000..d36e8d0e6
--- /dev/null
+++ b/synapse-electron/backend/utils/__init__.py
@@ -0,0 +1,36 @@
+"""
+Utilities package for Synapse Desktop Client backend.
+
+This package contains utility functions and helper classes used
+throughout the application.
+"""
+
+from .async_utils import run_async_task_in_background
+from .logging_utils import get_queued_messages, initialize_logging, setup_logging
+from .system_utils import (
+ get_home_and_downloads_directories,
+ scan_directory_for_files,
+ setup_electron_environment,
+)
+from .websocket_utils import (
+ broadcast_message,
+ handle_websocket_client,
+ start_websocket_server,
+)
+
+__all__ = [
+ # Logging utilities
+ "setup_logging",
+ "get_queued_messages",
+ "initialize_logging",
+ # WebSocket utilities
+ "handle_websocket_client",
+ "broadcast_message",
+ "start_websocket_server",
+ # System utilities
+ "setup_electron_environment",
+ "get_home_and_downloads_directories",
+ "scan_directory_for_files",
+ # Async utilities
+ "run_async_task_in_background",
+]
diff --git a/synapse-electron/backend/utils/async_utils.py b/synapse-electron/backend/utils/async_utils.py
new file mode 100644
index 000000000..930671e0a
--- /dev/null
+++ b/synapse-electron/backend/utils/async_utils.py
@@ -0,0 +1,65 @@
+"""
+Asynchronous utilities for the Synapse Desktop Client backend.
+
+This module provides utilities for managing background tasks and
+async operations without blocking the main event loop.
+"""
+
+import asyncio
+import logging
+import threading
+from typing import Any, Callable
+
+logger = logging.getLogger(__name__)
+
+
+def run_async_task_in_background(
+ async_task: Callable[[], Any], task_name: str = "background_task"
+) -> None:
+ """
+ Run an async task in a background thread with its own event loop.
+
+ This utility helps avoid blocking the main FastAPI event loop when
+ running long-running tasks like file uploads/downloads.
+
+ Arguments:
+ async_task: The async function to run in the background
+ task_name: Name for logging purposes
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Background task errors are logged but not propagated to caller.
+ """
+
+ def run_task_in_thread() -> None:
+ """
+ Run the async task in a new event loop.
+
+ Creates a new event loop for the background thread and executes
+ the async task within it.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Task execution errors are logged and handled gracefully.
+ """
+ try:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ loop.run_until_complete(async_task())
+ except Exception as e:
+ logger.error("Background task %s error: %s", task_name, e)
+ finally:
+ loop.close()
+
+ thread = threading.Thread(target=run_task_in_thread, name=task_name)
+ thread.daemon = True
+ thread.start()
+
+ logger.info("Started background task: %s", task_name)
diff --git a/synapse-electron/backend/utils/logging_utils.py b/synapse-electron/backend/utils/logging_utils.py
new file mode 100644
index 000000000..64d6d928a
--- /dev/null
+++ b/synapse-electron/backend/utils/logging_utils.py
@@ -0,0 +1,260 @@
+"""
+Logging utilities for the Synapse Desktop Client backend.
+
+This module provides centralized logging configuration and handlers
+for forwarding logs to the Electron frontend, with support for
+dynamic log level control.
+"""
+
+import logging
+from typing import List
+
+from models.api_models import LogMessage
+
+# Global message queue for logs to avoid asyncio issues
+log_message_queue: List[LogMessage] = []
+
+
+class ElectronLogHandler(logging.Handler):
+ """
+ Custom logging handler that forwards logs to Electron via message queue.
+
+ This handler captures log messages and stores them in a queue that can be
+ polled by the Electron frontend, avoiding the complexity of real-time
+ WebSocket communication for log messages.
+ """
+
+ def emit(self, record: logging.LogRecord) -> None:
+ """
+ Process a log record and add it to the message queue.
+
+ Processes incoming log records, formats them appropriately,
+ and adds them to the queue for frontend consumption.
+
+ Arguments:
+ record: The log record to process
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Record processing errors are caught and printed to avoid recursion.
+ """
+ try:
+ if self._should_skip_record(record):
+ return
+
+ log_data = self._create_log_message(record)
+ self._add_to_queue(log_data)
+
+ except Exception as e:
+ print(f"Error in ElectronLogHandler: {e}")
+
+ def _should_skip_record(self, record: logging.LogRecord) -> bool:
+ """
+ Check if a log record should be skipped.
+
+ Determines whether certain log records should be filtered out
+ to reduce noise in the frontend log viewer.
+
+ Arguments:
+ record: The log record to check
+
+ Returns:
+ bool: True if the record should be skipped
+
+ Raises:
+ None: This method does not raise exceptions.
+ """
+ return record.name.startswith("websockets")
+
+ def _create_log_message(self, record: logging.LogRecord) -> LogMessage:
+ """
+ Create a LogMessage from a log record.
+
+ Converts a Python logging record into a LogMessage object
+ suitable for frontend consumption.
+
+ Arguments:
+ record: The log record to convert
+
+ Returns:
+ LogMessage: LogMessage object ready for the frontend
+
+ Raises:
+ Exception: Message creation errors are allowed to propagate.
+ """
+ message = self.format(record)
+
+ level_mapping = {
+ logging.DEBUG: "debug",
+ logging.INFO: "info",
+ logging.WARNING: "warning",
+ logging.ERROR: "error",
+ logging.CRITICAL: "critical",
+ }
+ level = level_mapping.get(record.levelno, "info")
+
+ return LogMessage(
+ type="log",
+ message=message,
+ level=level,
+ logger_name=record.name,
+ timestamp=record.created,
+ source="python-logger",
+ auto_scroll=True,
+ raw_message=record.getMessage(),
+ filename=getattr(record, "filename", ""),
+ line_number=getattr(record, "lineno", 0),
+ )
+
+ def _add_to_queue(self, log_data: LogMessage) -> None:
+ """
+ Add log message to the queue with size management.
+
+ Adds the log message to the global queue and manages queue
+ size to prevent memory issues.
+
+ Arguments:
+ log_data: The log message to add
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Queue management errors are caught and printed.
+ """
+ try:
+ log_message_queue.append(log_data)
+
+ # Keep queue size reasonable - remove old messages if it gets too large
+ if len(log_message_queue) > 1000:
+ log_message_queue.pop(0)
+
+ except Exception as e:
+ print(f"Error queuing log message: {e}")
+
+
+def setup_logging(log_level: str = "info") -> None:
+ """
+ Configure logging for the application with specified log level.
+
+ Sets up the custom ElectronLogHandler to capture all log messages
+ and forward them to the frontend via the message queue.
+
+ Arguments:
+ log_level: Log level to set. Options: "debug", "info", "warning", "error"
+
+ Returns:
+ None
+
+ Raises:
+ None: Configuration errors are handled gracefully.
+ """
+ # Map string levels to logging constants
+ level_mapping = {
+ "debug": logging.DEBUG,
+ "info": logging.INFO,
+ "warning": logging.WARNING,
+ "error": logging.ERROR,
+ }
+
+ numeric_level = level_mapping.get(log_level.lower(), logging.INFO)
+
+ # Create and configure the custom handler
+ electron_handler = ElectronLogHandler()
+ electron_handler.setLevel(
+ logging.DEBUG
+ ) # Handler captures all, filtering happens at logger level
+
+ formatter = logging.Formatter(
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+ )
+ electron_handler.setFormatter(formatter)
+
+ # Configure root logger
+ root_logger = logging.getLogger()
+
+ # Remove existing handlers to avoid duplicates
+ for handler in root_logger.handlers[:]:
+ root_logger.removeHandler(handler)
+
+ root_logger.addHandler(electron_handler)
+ root_logger.setLevel(numeric_level)
+
+ # Set specific logger levels
+ _configure_specific_loggers(numeric_level)
+
+
+def _configure_specific_loggers(log_level: int) -> None:
+ """
+ Configure specific logger levels to reduce noise.
+
+ Sets appropriate log levels for various third-party and internal
+ loggers to maintain readability in the log output.
+
+ Arguments:
+ log_level: The numeric logging level to apply
+
+ Returns:
+ None
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ # WebSocket loggers - always keep at WARNING unless debug mode
+ websockets_level = logging.WARNING if log_level > logging.DEBUG else log_level
+ logging.getLogger("websockets").setLevel(websockets_level)
+ logging.getLogger("websockets.protocol").setLevel(websockets_level)
+
+ # Synapse client loggers - respect the main log level
+ logging.getLogger("synapseclient").setLevel(log_level)
+ logging.getLogger("synapseclient.core").setLevel(log_level)
+
+ # Third-party loggers that can be noisy
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
+ logging.getLogger("requests").setLevel(logging.WARNING)
+
+
+def get_queued_messages() -> list[LogMessage]:
+ """
+ Get all queued log messages and clear the queue.
+
+ Retrieves all pending log messages from the global queue and
+ clears the queue for the next polling cycle.
+
+ Arguments:
+ None
+
+ Returns:
+ list[LogMessage]: List of LogMessage objects from the queue
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ messages = log_message_queue.copy()
+ log_message_queue.clear()
+ return messages
+
+
+async def initialize_logging() -> None:
+ """
+ Initialize logging and send startup messages.
+
+ This function should be called during application startup to
+ emit initial log messages confirming the logging system is working.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Initialization errors are caught and printed.
+ """
+ try:
+ logger = logging.getLogger(__name__)
+ logger.info("Synapse Backend Server logging system initialized")
+ except Exception as e:
+ print(f"Failed to initialize logging: {e}")
diff --git a/synapse-electron/backend/utils/system_utils.py b/synapse-electron/backend/utils/system_utils.py
new file mode 100644
index 000000000..95d4ccf0b
--- /dev/null
+++ b/synapse-electron/backend/utils/system_utils.py
@@ -0,0 +1,385 @@
+"""
+System utilities for the Synapse Desktop Client backend.
+
+This module provides utilities for environment setup, directory operations,
+and system-specific functionality.
+"""
+
+import logging
+import mimetypes
+import os
+import sys
+from typing import Any, Dict, List, Optional
+
+logger = logging.getLogger(__name__)
+
+
+def setup_electron_environment() -> None:
+ """
+ Setup environment variables and directories when running from Electron.
+
+ This fixes issues with temporary directories and cache paths in packaged apps.
+ Detects if running in an Electron context and configures appropriate paths.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ OSError: If directory creation or environment setup fails.
+ IOError: If file system operations fail.
+ """
+ try:
+ is_electron_context = _detect_electron_environment()
+
+ if not is_electron_context:
+ logger.info("Not in Electron environment, using default settings")
+ return
+
+ logger.info("Detected Electron environment, setting up proper directories")
+
+ app_cache_dir = _get_app_cache_directory()
+ _setup_cache_directories(app_cache_dir)
+ _setup_temp_directories(app_cache_dir)
+ _change_working_directory()
+
+ except (OSError, IOError) as e:
+ logger.warning("Failed to setup Electron environment: %s", e)
+
+
+def _detect_electron_environment() -> bool:
+ """
+ Check if we're running from within Electron's environment.
+
+ Examines various environment indicators to determine if the
+ application is running within an Electron context.
+
+ Arguments:
+ None
+
+ Returns:
+ bool: True if running in Electron context, False otherwise
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ return (
+ "electron" in sys.executable.lower()
+ or "resources" in os.getcwd()
+ or os.path.exists(os.path.join(os.getcwd(), "..", "..", "app.asar"))
+ )
+
+
+def _get_app_cache_directory() -> str:
+ """
+ Get the application cache directory for the current platform.
+
+ Determines the appropriate cache directory based on the operating
+ system and available environment variables.
+
+ Arguments:
+ None
+
+ Returns:
+ str: Path to the application cache directory
+
+ Raises:
+ None: This function provides fallback paths for all scenarios.
+ """
+ if os.name == "nt": # Windows
+ cache_base = os.getenv("LOCALAPPDATA") or os.getenv("APPDATA")
+ if cache_base:
+ return os.path.join(cache_base, "SynapseDesktopClient")
+ else:
+ return os.path.join(
+ os.path.expanduser("~"),
+ "AppData",
+ "Local",
+ "SynapseDesktopClient",
+ )
+ else: # Unix-like systems
+ cache_base = os.getenv("XDG_CACHE_HOME")
+ if cache_base:
+ return os.path.join(cache_base, "SynapseDesktopClient")
+ else:
+ return os.path.join(
+ os.path.expanduser("~"), ".cache", "SynapseDesktopClient"
+ )
+
+
+def _setup_cache_directories(app_cache_dir: str) -> None:
+ """
+ Create and configure cache directories.
+
+ Sets up the application cache directories and configures
+ environment variables for Synapse client caching.
+
+ Arguments:
+ app_cache_dir: Base application cache directory
+
+ Returns:
+ None
+
+ Raises:
+ OSError: If directory creation fails.
+ """
+ os.makedirs(app_cache_dir, exist_ok=True)
+
+ synapse_cache = os.path.join(app_cache_dir, "synapse")
+ os.makedirs(synapse_cache, exist_ok=True)
+ os.environ["SYNAPSE_CACHE"] = synapse_cache
+
+ logger.info("Set SYNAPSE_CACHE to: %s", synapse_cache)
+
+
+def _setup_temp_directories(app_cache_dir: str) -> None:
+ """
+ Create and configure temporary directories.
+
+ Sets up application-specific temporary directories and configures
+ environment variables to use them.
+
+ Arguments:
+ app_cache_dir: Base application cache directory
+
+ Returns:
+ None
+
+ Raises:
+ OSError: If directory creation fails.
+ """
+ temp_dir = os.path.join(app_cache_dir, "temp")
+ os.makedirs(temp_dir, exist_ok=True)
+
+ os.environ["TMPDIR"] = temp_dir
+ os.environ["TMP"] = temp_dir
+ os.environ["TEMP"] = temp_dir
+
+ logger.info("Set temp directories to: %s", temp_dir)
+
+
+def _change_working_directory() -> None:
+ """
+ Change working directory to user's home to avoid permission issues.
+
+ Changes the current working directory to the user's home directory
+ to avoid potential permission issues in packaged app environments.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ OSError: If directory change fails (logged but not propagated).
+ """
+ user_home = os.path.expanduser("~")
+ os.chdir(user_home)
+ logger.info("Changed working directory to: %s", user_home)
+
+
+def get_home_and_downloads_directories() -> Dict[str, str]:
+ """
+ Get the user's home and downloads directory paths.
+
+ Retrieves the user's home directory and downloads directory paths,
+ creating the downloads directory if it doesn't exist.
+
+ Arguments:
+ None
+
+ Returns:
+ Dict[str, str]: Dictionary with 'home_directory' and 'downloads_directory' keys
+
+ Raises:
+ Exception: If directories cannot be accessed or created
+ """
+ home_dir = os.path.expanduser("~")
+ downloads_dir = os.path.join(home_dir, "Downloads")
+
+ # Verify the Downloads directory exists, create if it doesn't
+ if not os.path.exists(downloads_dir):
+ try:
+ os.makedirs(downloads_dir, exist_ok=True)
+ logger.info("Created Downloads directory: %s", downloads_dir)
+ except (OSError, IOError) as e:
+ logger.warning("Could not create Downloads directory: %s", e)
+ downloads_dir = home_dir
+
+ return {"home_directory": home_dir, "downloads_directory": downloads_dir}
+
+
+def scan_directory_for_files(
+ directory_path: str, recursive: bool = True
+) -> Dict[str, Any]:
+ """
+ Scan a directory for files and folders with metadata.
+
+ Recursively scans a directory and collects metadata about all files
+ and folders found, including size, type, and path information.
+
+ Arguments:
+ directory_path: The directory path to scan
+ recursive: Whether to scan subdirectories recursively
+
+ Returns:
+ Dict[str, Any]: Dictionary containing file list and summary information with:
+ - success: Boolean indicating scan success
+ - files: List of file/folder metadata
+ - summary: Summary statistics about scanned items
+
+ Raises:
+ ValueError: If directory doesn't exist or isn't a directory
+ OSError: If directory access fails
+ """
+ if not os.path.exists(directory_path):
+ raise ValueError("Directory does not exist")
+
+ if not os.path.isdir(directory_path):
+ raise ValueError("Path is not a directory")
+
+ logger.info("Scanning directory: %s", directory_path)
+
+ files = []
+ total_size = 0
+
+ def scan_recursive(current_path: str, base_path: str) -> List[Dict[str, Any]]:
+ """
+ Recursively scan a directory.
+
+ Performs recursive directory scanning to collect file and folder
+ metadata at all levels.
+
+ Arguments:
+ current_path: Current directory being scanned
+ base_path: Base directory for relative path calculation
+
+ Returns:
+ List[Dict[str, Any]]: List of file/folder metadata dictionaries
+
+ Raises:
+ PermissionError: If directory access is denied (logged and handled)
+ OSError: If directory operations fail (logged and handled)
+ """
+ nonlocal total_size
+ items = []
+
+ try:
+ for item in os.listdir(current_path):
+ item_path = os.path.join(current_path, item)
+ relative_path = os.path.relpath(item_path, base_path)
+
+ if os.path.isfile(item_path):
+ file_info = _get_file_info(
+ item, item_path, relative_path, current_path
+ )
+ if file_info:
+ items.append(file_info)
+ total_size += file_info["size"]
+
+ elif os.path.isdir(item_path) and recursive:
+ folder_info = _get_folder_info(
+ item, item_path, relative_path, current_path
+ )
+ items.append(folder_info)
+
+ # Recursively scan subdirectories
+ sub_items = scan_recursive(item_path, base_path)
+ items.extend(sub_items)
+
+ except (PermissionError, OSError) as e:
+ logger.warning("Could not access directory %s: %s", current_path, e)
+
+ return items
+
+ files = scan_recursive(directory_path, directory_path)
+
+ # Count files vs folders
+ file_count = sum(1 for f in files if f["type"] == "file")
+ folder_count = sum(1 for f in files if f["type"] == "folder")
+
+ logger.info("Found %d files and %d folders", file_count, folder_count)
+
+ return {
+ "success": True,
+ "files": files,
+ "summary": {
+ "total_items": len(files),
+ "file_count": file_count,
+ "folder_count": folder_count,
+ "total_size": total_size,
+ },
+ }
+
+
+def _get_file_info(
+ item_name: str, item_path: str, relative_path: str, current_path: str
+) -> Optional[Dict[str, Any]]:
+ """
+ Get file information for a single file.
+
+ Collects metadata for a single file including size, type, and path information.
+
+ Arguments:
+ item_name: Name of the file
+ item_path: Full path to the file
+ relative_path: Relative path from scan root
+ current_path: Parent directory path
+
+ Returns:
+ Optional[Dict[str, Any]]: File information dictionary or None if file cannot be accessed
+
+ Raises:
+ OSError: If file access fails (caught and handled gracefully)
+ IOError: If file operations fail (caught and handled gracefully)
+ """
+ try:
+ file_size = os.path.getsize(item_path)
+ mime_type, _ = mimetypes.guess_type(item_path)
+
+ return {
+ "id": item_path,
+ "name": item_name,
+ "type": "file",
+ "size": file_size,
+ "path": item_path,
+ "relative_path": relative_path,
+ "parent_path": current_path,
+ "mime_type": mime_type or "application/octet-stream",
+ }
+ except (OSError, IOError) as e:
+ logger.warning("Could not access file %s: %s", item_path, e)
+ return None
+
+
+def _get_folder_info(
+ item_name: str, item_path: str, relative_path: str, current_path: str
+) -> Dict[str, Any]:
+ """
+ Get folder information for a single folder.
+
+ Collects metadata for a single folder including name and path information.
+
+ Arguments:
+ item_name: Name of the folder
+ item_path: Full path to the folder
+ relative_path: Relative path from scan root
+ current_path: Parent directory path
+
+ Returns:
+ Dict[str, Any]: Folder information dictionary
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ return {
+ "id": item_path,
+ "name": item_name,
+ "type": "folder",
+ "path": item_path,
+ "relative_path": relative_path,
+ "parent_path": current_path,
+ }
diff --git a/synapse-electron/backend/utils/websocket_utils.py b/synapse-electron/backend/utils/websocket_utils.py
new file mode 100644
index 000000000..1cde39338
--- /dev/null
+++ b/synapse-electron/backend/utils/websocket_utils.py
@@ -0,0 +1,225 @@
+"""
+WebSocket utilities for the Synapse Desktop Client backend.
+
+This module handles WebSocket connections and message broadcasting
+to the Electron frontend.
+"""
+
+import asyncio
+import json
+import logging
+import threading
+from typing import Any, Dict, Optional, Set
+
+import websockets
+from websockets.exceptions import ConnectionClosed
+
+logger = logging.getLogger(__name__)
+
+# Global set to track connected WebSocket clients
+connected_clients: Set[Any] = set()
+
+
+async def handle_websocket_client(websocket: Any, path: Optional[str] = None) -> None:
+ """
+ Handle individual WebSocket connections from the Electron frontend.
+
+ Manages the lifecycle of a WebSocket connection including registration,
+ message handling, and cleanup when the connection ends.
+
+ Arguments:
+ websocket: The WebSocket connection object
+ path: The WebSocket connection path (optional)
+
+ Returns:
+ None
+
+ Raises:
+ ConnectionClosed: When the WebSocket connection is closed (handled gracefully)
+ Exception: Other WebSocket errors are caught and logged
+ """
+ connected_clients.add(websocket)
+ logger.info(
+ f"WebSocket client connected from path: {path}. "
+ f"Total clients: {len(connected_clients)}"
+ )
+
+ try:
+ # Send connection confirmation
+ await websocket.send(
+ json.dumps({"type": "connection_status", "connected": True})
+ )
+
+ # Listen for incoming messages
+ async for message in websocket:
+ logger.info(f"Received WebSocket message: {message}")
+
+ except ConnectionClosed:
+ logger.info("WebSocket client disconnected")
+ except Exception as e:
+ logger.error(f"WebSocket error: {e}")
+ finally:
+ connected_clients.discard(websocket)
+ logger.info(
+ f"WebSocket client removed. Total clients: {len(connected_clients)}"
+ )
+
+
+async def broadcast_message(message: Dict[str, Any]) -> None:
+ """
+ Broadcast a message to all connected WebSocket clients.
+
+ Sends a message to all currently connected WebSocket clients,
+ handling disconnections gracefully and cleaning up dead connections.
+
+ Arguments:
+ message: The message dictionary to broadcast
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Broadcast errors are handled gracefully per client.
+ """
+ if not connected_clients:
+ return _log_no_clients_message(message)
+
+ _add_message_metadata(message)
+
+ # Create a copy to avoid "set changed size during iteration" error
+ clients_copy = connected_clients.copy()
+ disconnected = set()
+ message_json = json.dumps(message)
+
+ for client in clients_copy:
+ try:
+ await client.send(message_json)
+ except ConnectionClosed:
+ disconnected.add(client)
+ except Exception as e:
+ logger.warning(f"Failed to send message to client: {e}")
+ disconnected.add(client)
+
+ # Remove disconnected clients
+ for client in disconnected:
+ connected_clients.discard(client)
+
+
+def _log_no_clients_message(message: Dict[str, Any]) -> None:
+ """
+ Log when no clients are connected, avoiding spam for frequent messages.
+
+ Provides selective logging to avoid spamming logs when no WebSocket
+ clients are connected, filtering out frequent message types.
+
+ Arguments:
+ message: The message that couldn't be sent
+
+ Returns:
+ None
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ message_type = message.get("type", "unknown")
+ if message_type not in ["log", "progress"]:
+ logger.debug(f"No WebSocket clients connected to send message: {message_type}")
+
+
+def _add_message_metadata(message: Dict[str, Any]) -> None:
+ """
+ Add metadata to outgoing messages.
+
+ Enhances outgoing messages with additional metadata such as
+ timestamps and UI hints for better frontend handling.
+
+ Arguments:
+ message: The message to add metadata to
+
+ Returns:
+ None
+
+ Raises:
+ None: This function does not raise exceptions.
+ """
+ # Add auto-scroll flag for log messages
+ if message.get("type") == "log":
+ message["auto_scroll"] = True
+
+ # Add timestamp if not present
+ if "timestamp" not in message:
+ import time
+
+ message["timestamp"] = time.time()
+
+
+def start_websocket_server(port: int) -> None:
+ """
+ Start the WebSocket server in a separate thread.
+
+ Initializes and starts the WebSocket server in its own thread with
+ a dedicated event loop to avoid blocking the main application.
+
+ Arguments:
+ port: The port number to bind the WebSocket server to
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Server startup errors are caught and logged.
+ """
+
+ def run_websocket_server() -> None:
+ """
+ Run the WebSocket server in its own event loop.
+
+ Creates a new event loop and runs the WebSocket server within it,
+ handling all WebSocket connections independently of the main application.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Server runtime errors are caught and logged.
+ """
+
+ async def websocket_server() -> None:
+ """
+ Create and run the WebSocket server.
+
+ Sets up the WebSocket server with proper handlers and runs
+ it indefinitely until the application shuts down.
+
+ Arguments:
+ None
+
+ Returns:
+ None
+
+ Raises:
+ Exception: Server creation and runtime errors are caught and logged.
+ """
+
+ async def websocket_handler(
+ websocket: Any, path: Optional[str] = None
+ ) -> None:
+ await handle_websocket_client(websocket, path)
+
+ try:
+ async with websockets.serve(websocket_handler, "localhost", port):
+ logger.info("WebSocket server started on ws://localhost:%s", port)
+ await asyncio.Future() # Run forever
+ except Exception:
+ logger.exception("WebSocket server error")
+
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ loop.run_until_complete(websocket_server())
+
+ ws_thread = threading.Thread(target=run_websocket_server)
+ ws_thread.daemon = True
+ ws_thread.start()
diff --git a/synapse-electron/main.js b/synapse-electron/main.js
new file mode 100644
index 000000000..cda196255
--- /dev/null
+++ b/synapse-electron/main.js
@@ -0,0 +1,695 @@
+const { app, BrowserWindow, ipcMain, dialog, shell, session } = require('electron');
+const path = require('path');
+const { spawn } = require('child_process');
+const axios = require('axios');
+const WebSocket = require('ws');
+const log = require('electron-log');
+
+class SynapseElectronApp {
+ constructor() {
+ this.pythonProcess = null;
+ this.mainWindow = null;
+ this.backendPort = 8000;
+ this.wsPort = 8001;
+ this.websocketServer = null;
+ this.connectedClients = new Set();
+ }
+
+ async startPythonBackend() {
+ log.info('Starting Python backend...');
+
+ // Determine Python executable path
+ let pythonExe;
+ let backendPath;
+
+ if (app.isPackaged) {
+ // In packaged app, use the compiled backend
+ backendPath = path.join(process.resourcesPath, 'backend');
+
+ if (process.platform === 'win32') {
+ pythonExe = path.join(backendPath, 'synapse-backend.exe');
+ } else {
+ pythonExe = path.join(backendPath, 'synapse-backend');
+ }
+
+ log.info(`Using packaged backend: ${pythonExe}`);
+
+ this.pythonProcess = spawn(pythonExe, [
+ '--port', this.backendPort.toString(),
+ '--ws-port', this.wsPort.toString()
+ ], {
+ stdio: ['pipe', 'pipe', 'pipe'],
+ cwd: backendPath
+ });
+ } else {
+ // In development, use the Python script
+ backendPath = path.join(__dirname, 'backend');
+
+ if (process.platform === 'win32') {
+ // On Windows, try to use the venv first, fallback to system python
+ const venvPython = path.join(backendPath, 'venv', 'Scripts', 'python.exe');
+ if (require('fs').existsSync(venvPython)) {
+ pythonExe = venvPython;
+ log.info(`Using virtual environment Python: ${pythonExe}`);
+ } else {
+ pythonExe = 'python.exe';
+ log.info('Virtual environment not found, using system Python');
+ }
+ } else {
+ pythonExe = 'python3';
+ }
+
+ this.pythonProcess = spawn(pythonExe, [
+ path.join(backendPath, 'server.py'),
+ '--port', this.backendPort.toString(),
+ '--ws-port', this.wsPort.toString()
+ ], {
+ stdio: ['pipe', 'pipe', 'pipe'],
+ cwd: backendPath
+ });
+ }
+
+ this.pythonProcess.stdout.on('data', (data) => {
+ log.info(`Python Backend: ${data.toString()}`);
+ });
+
+ this.pythonProcess.stderr.on('data', (data) => {
+ log.error(`Python Backend Error: ${data.toString()}`);
+ });
+
+ this.pythonProcess.on('close', (code) => {
+ log.info(`Python backend exited with code ${code}`);
+ });
+
+ this.pythonProcess.on('error', (error) => {
+ log.error(`Failed to start Python backend: ${error.message}`);
+ });
+
+ // Wait for backend to be ready
+ await this.waitForBackend();
+
+ // Start WebSocket server connection
+ this.setupWebSocketConnection();
+ }
+
+ setupWebSocketConnection() {
+ const wsUrl = `ws://127.0.0.1:${this.wsPort}`;
+
+ const connectWebSocket = () => {
+ try {
+ this.ws = new WebSocket(wsUrl);
+
+ this.ws.on('open', () => {
+ log.info('WebSocket connected to Python backend');
+ });
+
+ this.ws.on('message', (data) => {
+ try {
+ const message = JSON.parse(data.toString());
+ this.handleWebSocketMessage(message);
+ } catch (error) {
+ log.error('Error parsing WebSocket message:', error);
+ }
+ });
+
+ this.ws.on('close', () => {
+ log.warn('WebSocket connection closed, attempting to reconnect...');
+ setTimeout(connectWebSocket, 2000);
+ });
+
+ this.ws.on('error', (error) => {
+ log.error('WebSocket error:', error);
+ });
+ } catch (error) {
+ log.error('Failed to connect WebSocket:', error);
+ setTimeout(connectWebSocket, 2000);
+ }
+ };
+
+ connectWebSocket();
+ }
+
+ handleWebSocketMessage(message) {
+ // Forward WebSocket messages to renderer process
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
+ this.mainWindow.webContents.send('websocket-message', message);
+ }
+ }
+
+ async waitForBackend(maxRetries = 30) {
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ log.info(`Attempt ${i + 1}/${maxRetries}: Checking backend health at http://127.0.0.1:${this.backendPort}/health`);
+ await axios.get(`http://127.0.0.1:${this.backendPort}/health`);
+ log.info('Python backend is ready');
+ return;
+ } catch (error) {
+ log.info(`Backend not ready yet (attempt ${i + 1}): ${error.message}`);
+ if (i === maxRetries - 1) {
+ throw new Error('Python backend failed to start within timeout');
+ }
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+ }
+ }
+
+ createWindow() {
+ // Check if we're running in a headless environment
+ const isHeadless = process.env.DISPLAY === ':99' || process.env.CI || process.env.HEADLESS;
+
+ const webPrefs = {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, 'preload.js'),
+ webSecurity: true,
+ // Force software rendering to avoid GPU issues
+ hardwareAcceleration: false,
+ // Additional security settings
+ allowRunningInsecureContent: false,
+ experimentalFeatures: false
+ };
+
+ // Add offscreen rendering for headless mode
+ if (isHeadless) {
+ webPrefs.offscreen = true;
+ }
+
+ this.mainWindow = new BrowserWindow({
+ width: 1200,
+ height: 800,
+ minWidth: 800,
+ minHeight: 600,
+ webPreferences: webPrefs,
+ icon: path.join(__dirname, 'assets', 'icon.png'),
+ show: !isHeadless // Don't show in headless mode
+ });
+
+ this.mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'))
+ .then(() => {
+ log.info('HTML file loaded successfully');
+ })
+ .catch((error) => {
+ log.error('Failed to load HTML file:', error);
+ });
+
+ // Show window when ready (only if not headless)
+ if (!isHeadless) {
+ this.mainWindow.once('ready-to-show', () => {
+ this.mainWindow.show();
+
+ // Center window on screen
+ this.mainWindow.center();
+ });
+ } else {
+ console.log('Running in headless mode - window will not be displayed');
+ }
+
+ // Handle window closed
+ this.mainWindow.on('closed', () => {
+ this.mainWindow = null;
+ });
+
+ // Add debugging for web contents
+ this.mainWindow.webContents.on('did-finish-load', () => {
+ log.info('WebContents finished loading');
+ });
+
+ this.mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
+ log.error('WebContents failed to load:', errorCode, errorDescription);
+ });
+
+ // Development: Open DevTools
+ if (!app.isPackaged) {
+ this.mainWindow.webContents.openDevTools();
+ }
+
+ // Configure CORS security for the renderer process
+ this.setupCORSSecurity();
+ }
+
+ setupCORSSecurity() {
+ const session = this.mainWindow.webContents.session;
+
+ // Define allowed origins that match the backend CORS configuration
+ const allowedOrigins = [
+ `http://localhost:${this.backendPort}`,
+ `http://127.0.0.1:${this.backendPort}`,
+ 'file://',
+ // Allow any localhost port for development flexibility
+ /^http:\/\/localhost:\d+$/,
+ /^http:\/\/127\.0\.0\.1:\d+$/
+ ];
+
+ // Define the Content Security Policy
+ const cspPolicy = [
+ `default-src 'self' http://localhost:* http://127.0.0.1:* file:`,
+ `script-src 'self' 'unsafe-inline' http://localhost:* http://127.0.0.1:* https://cdnjs.cloudflare.com`,
+ `style-src 'self' 'unsafe-inline' http://localhost:* http://127.0.0.1:* https://cdnjs.cloudflare.com`,
+ `img-src 'self' data: blob: http://localhost:* http://127.0.0.1:* https://cdnjs.cloudflare.com`,
+ `connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://localhost:* http://127.0.0.1:*`,
+ `font-src 'self' data: https://cdnjs.cloudflare.com`,
+ `object-src 'none'`,
+ `base-uri 'self'`,
+ `form-action 'self'`
+ ].join('; ');
+
+ // Set up request filtering for enhanced security
+ session.webRequest.onBeforeRequest((details, callback) => {
+ const url = new URL(details.url);
+
+ // Allow local file protocol for the app's own files
+ if (url.protocol === 'file:') {
+ callback({ cancel: false });
+ return;
+ }
+
+ // Allow DevTools protocol and related resources
+ if (url.protocol === 'devtools:' ||
+ url.hostname === 'chrome-devtools-frontend.appspot.com') {
+ callback({ cancel: false });
+ return;
+ }
+
+ // Allow requests to backend server
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
+ callback({ cancel: false });
+ return;
+ }
+
+ // Allow requests to trusted CDNs
+ if (url.hostname === 'cdnjs.cloudflare.com') {
+ callback({ cancel: false });
+ return;
+ }
+
+ // Block all other external requests for security
+ log.warn(`Blocked request to external origin: ${details.url}`);
+ callback({ cancel: true });
+ });
+
+ // Set up response headers for additional security
+ session.webRequest.onHeadersReceived((details, callback) => {
+ const responseHeaders = details.responseHeaders || {};
+ const url = new URL(details.url);
+
+ // Don't apply CSP to DevTools resources
+ if (url.protocol === 'devtools:' || url.hostname === 'chrome-devtools-frontend.appspot.com') {
+ callback({ responseHeaders });
+ return;
+ }
+
+ // Add security headers for app resources only
+ responseHeaders['X-Content-Type-Options'] = ['nosniff'];
+ responseHeaders['X-Frame-Options'] = ['DENY'];
+ responseHeaders['X-XSS-Protection'] = ['1; mode=block'];
+ responseHeaders['Referrer-Policy'] = ['strict-origin-when-cross-origin'];
+
+ // Set Content Security Policy for app resources only
+ // Force override any existing CSP
+ responseHeaders['Content-Security-Policy'] = [cspPolicy];
+
+ callback({ responseHeaders });
+ });
+
+ // Also set CSP on the main frame directly
+ session.webRequest.onBeforeSendHeaders((details, callback) => {
+ // Inject CSP header for main frame requests
+ if (details.resourceType === 'mainFrame') {
+ const requestHeaders = details.requestHeaders || {};
+ callback({ requestHeaders });
+ } else {
+ callback({});
+ }
+ });
+
+ // Set CSP directly on webContents for immediate effect
+ this.mainWindow.webContents.on('dom-ready', () => {
+ // Only apply to main window, not DevTools
+ if (this.mainWindow.webContents.getURL().startsWith('file://')) {
+ this.mainWindow.webContents.executeJavaScript(`
+ (function(cspContent) {
+ // Remove any existing CSP meta tags that might conflict
+ const existingCSPs = document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]');
+ existingCSPs.forEach(csp => csp.remove());
+
+ // Add our CSP meta tag
+ const meta = document.createElement('meta');
+ meta.setAttribute('http-equiv', 'Content-Security-Policy');
+ meta.setAttribute('content', cspContent);
+ document.head.appendChild(meta);
+
+ // Log for debugging
+ console.log('CSP applied:', cspContent);
+ })('${cspPolicy.replace(/'/g, "\\'")}');
+ `).catch(err => {
+ log.error('Failed to inject CSP:', err);
+ });
+ }
+ });
+
+ log.info('CORS security configuration applied');
+ }
+
+ setupIPC() {
+ // Authentication endpoints
+ ipcMain.handle('synapse-login', async (event, credentials) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/auth/login`, credentials);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ ipcMain.handle('synapse-logout', async (event) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/auth/logout`);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ ipcMain.handle('synapse-get-profiles', async (event) => {
+ try {
+ const response = await axios.get(`http://127.0.0.1:${this.backendPort}/auth/profiles`);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ // File operations
+ ipcMain.handle('synapse-upload', async (event, uploadData) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/files/upload`, uploadData);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ ipcMain.handle('synapse-download', async (event, downloadData) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/files/download`, downloadData);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ // Bulk operations
+ ipcMain.handle('synapse-scan-directory', async (event, scanData) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/files/scan-directory`, scanData);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ ipcMain.handle('synapse-enumerate', async (event, containerData) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/bulk/enumerate`, containerData);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ ipcMain.handle('synapse-bulk-download', async (event, bulkData) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/bulk/download`, bulkData);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ ipcMain.handle('synapse-bulk-upload', async (event, bulkData) => {
+ try {
+ const response = await axios.post(`http://127.0.0.1:${this.backendPort}/bulk/upload`, bulkData);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+
+ // File dialogs
+ ipcMain.handle('show-open-dialog', async (event, options) => {
+ const result = await dialog.showOpenDialog(this.mainWindow, options);
+ return result;
+ });
+
+ ipcMain.handle('show-save-dialog', async (event, options) => {
+ const result = await dialog.showSaveDialog(this.mainWindow, options);
+ return result;
+ });
+
+ // External links
+ ipcMain.handle('open-external', async (event, url) => {
+ await shell.openExternal(url);
+ });
+
+ // Application info
+ ipcMain.handle('get-app-version', () => {
+ return app.getVersion();
+ });
+
+ // System utilities
+ ipcMain.handle('get-home-directory', async (event) => {
+ try {
+ const response = await axios.get(`http://127.0.0.1:${this.backendPort}/system/home-directory`);
+ return { success: true, data: response.data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.response?.data?.detail || error.message
+ };
+ }
+ });
+ }
+
+ async initialize() {
+ await app.whenReady();
+
+ // Configure global security settings
+ this.setupGlobalSecurity();
+
+ try {
+ await this.startPythonBackend();
+ this.createWindow();
+ this.setupIPC();
+
+ log.info('Synapse Electron app initialized successfully');
+ } catch (error) {
+ log.error('Failed to initialize app:', error);
+
+ // Show error dialog and exit
+ dialog.showErrorBox(
+ 'Startup Error',
+ `Failed to start the application: ${error.message}`
+ );
+ app.quit();
+ }
+ }
+
+ setupGlobalSecurity() {
+ // Prevent new window creation with insecure settings
+ app.on('web-contents-created', (event, contents) => {
+ contents.on('new-window', (event, navigationUrl) => {
+ const parsedUrl = new URL(navigationUrl);
+
+ // Allow DevTools protocol
+ if (parsedUrl.protocol === 'devtools:') {
+ return;
+ }
+
+ // Only allow localhost and local file URLs
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'file:') {
+ event.preventDefault();
+ log.warn(`Blocked new window with protocol: ${parsedUrl.protocol}`);
+ return;
+ }
+
+ if (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:') {
+ if (parsedUrl.hostname !== 'localhost' && parsedUrl.hostname !== '127.0.0.1') {
+ event.preventDefault();
+ log.warn(`Blocked new window to external host: ${parsedUrl.hostname}`);
+ return;
+ }
+ }
+ });
+
+ // Prevent navigation to external URLs
+ contents.on('will-navigate', (event, navigationUrl) => {
+ const parsedUrl = new URL(navigationUrl);
+
+ // Allow file protocol for local files
+ if (parsedUrl.protocol === 'file:') {
+ return;
+ }
+
+ // Allow DevTools protocol
+ if (parsedUrl.protocol === 'devtools:') {
+ return;
+ }
+
+ // Allow localhost/127.0.0.1 only
+ if (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:') {
+ if (parsedUrl.hostname !== 'localhost' && parsedUrl.hostname !== '127.0.0.1') {
+ event.preventDefault();
+ log.warn(`Blocked navigation to external URL: ${navigationUrl}`);
+ return;
+ }
+ }
+ });
+ });
+
+ // Set additional app-level security
+ app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
+ // For localhost development, we might need to handle self-signed certificates
+ const parsedUrl = new URL(url);
+ if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {
+ // Allow certificate errors for localhost in development
+ if (!app.isPackaged) {
+ event.preventDefault();
+ callback(true);
+ return;
+ }
+ }
+
+ // Deny all other certificate errors
+ callback(false);
+ });
+
+ log.info('Global security configuration applied');
+ }
+
+ cleanup() {
+ log.info('Cleaning up application...');
+
+ if (this.ws) {
+ this.ws.close();
+ }
+
+ if (this.pythonProcess) {
+ this.pythonProcess.kill('SIGTERM');
+
+ // Force kill after 5 seconds if still running
+ setTimeout(() => {
+ if (this.pythonProcess && !this.pythonProcess.killed) {
+ this.pythonProcess.kill('SIGKILL');
+ }
+ }, 5000);
+ }
+ }
+}
+
+// Initialize the application
+const synapseApp = new SynapseElectronApp();
+
+// Add command line arguments for better headless support on Linux
+if (process.platform === 'linux') {
+ app.commandLine.appendSwitch('disable-dev-shm-usage');
+ app.commandLine.appendSwitch('disable-gpu');
+ app.commandLine.appendSwitch('disable-software-rasterizer');
+ app.commandLine.appendSwitch('disable-background-timer-throttling');
+ app.commandLine.appendSwitch('disable-renderer-backgrounding');
+ app.commandLine.appendSwitch('disable-features', 'VizDisplayCompositor');
+
+ // Check if we're in a headless environment
+ if (process.env.DISPLAY === ':99' || process.env.CI || process.env.HEADLESS) {
+ app.commandLine.appendSwitch('headless');
+ app.commandLine.appendSwitch('disable-gpu-sandbox');
+ app.commandLine.appendSwitch('no-sandbox');
+ }
+}
+
+// Add Windows-specific GPU debugging flags
+if (process.platform === 'win32') {
+ // Fix GPU issues on Windows by disabling GPU acceleration
+ app.commandLine.appendSwitch('disable-gpu');
+ app.commandLine.appendSwitch('disable-gpu-sandbox');
+ app.commandLine.appendSwitch('disable-software-rasterizer');
+ app.commandLine.appendSwitch('disable-gpu-compositing');
+ app.commandLine.appendSwitch('disable-gpu-rasterization');
+ app.commandLine.appendSwitch('disable-gpu-memory-buffer-compositor-resources');
+ app.commandLine.appendSwitch('disable-gpu-memory-buffer-video-frames');
+ app.commandLine.appendSwitch('enable-logging');
+ app.commandLine.appendSwitch('log-level', '0');
+
+ // Use software rendering and disable GPU context creation
+ app.commandLine.appendSwitch('disable-features', 'VizDisplayCompositor');
+ app.commandLine.appendSwitch('disable-d3d11');
+ app.commandLine.appendSwitch('disable-d3d9');
+ app.commandLine.appendSwitch('disable-webgl');
+ app.commandLine.appendSwitch('disable-webgl2');
+ app.commandLine.appendSwitch('disable-accelerated-2d-canvas');
+ app.commandLine.appendSwitch('disable-accelerated-jpeg-decoding');
+ app.commandLine.appendSwitch('disable-accelerated-mjpeg-decode');
+ app.commandLine.appendSwitch('disable-accelerated-video-decode');
+
+ log.info('Applied Windows GPU compatibility flags');
+}
+
+// Disable hardware acceleration completely
+app.disableHardwareAcceleration();
+
+// App event handlers
+app.on('ready', () => {
+ synapseApp.initialize();
+});
+
+app.on('window-all-closed', () => {
+ synapseApp.cleanup();
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
+
+app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ synapseApp.createWindow();
+ }
+});
+
+app.on('before-quit', () => {
+ synapseApp.cleanup();
+});
+
+// Handle uncaught exceptions
+process.on('uncaughtException', (error) => {
+ log.error('Uncaught Exception:', error);
+});
+
+process.on('unhandledRejection', (reason, promise) => {
+ log.error('Unhandled Rejection at:', promise, 'reason:', reason);
+});
diff --git a/synapse-electron/package-lock.json b/synapse-electron/package-lock.json
new file mode 100644
index 000000000..d200f3bfa
--- /dev/null
+++ b/synapse-electron/package-lock.json
@@ -0,0 +1,5175 @@
+{
+ "name": "synapse-desktop-client",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "synapse-desktop-client",
+ "version": "1.0.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "axios": "^1.5.0",
+ "cors": "^2.8.5",
+ "electron-log": "^4.4.8",
+ "electron-updater": "^6.1.4",
+ "express": "^4.18.2",
+ "multer": "^1.4.5-lts.1",
+ "ws": "^8.14.2"
+ },
+ "devDependencies": {
+ "concurrently": "^8.2.2",
+ "cross-env": "^10.0.0",
+ "electron": "^27.0.0",
+ "electron-builder": "^24.6.4"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
+ "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@develar/schema-utils": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
+ "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.0",
+ "ajv-keywords": "^3.4.1"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/@electron/asar": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
+ "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^5.0.0",
+ "glob": "^7.1.6",
+ "minimatch": "^3.0.4"
+ },
+ "bin": {
+ "asar": "bin/asar.js"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/@electron/asar/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@electron/asar/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@electron/get": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
+ "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "env-paths": "^2.2.0",
+ "fs-extra": "^8.1.0",
+ "got": "^11.8.5",
+ "progress": "^2.0.3",
+ "semver": "^6.2.0",
+ "sumchecker": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "global-agent": "^3.0.0"
+ }
+ },
+ "node_modules/@electron/notarize": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz",
+ "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.1",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/osx-sign": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz",
+ "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "compare-version": "^0.1.2",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.0.0",
+ "isbinaryfile": "^4.0.8",
+ "minimist": "^1.2.6",
+ "plist": "^3.0.5"
+ },
+ "bin": {
+ "electron-osx-flat": "bin/electron-osx-flat.js",
+ "electron-osx-sign": "bin/electron-osx-sign.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/isbinaryfile": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
+ "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/universal": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz",
+ "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/asar": "^3.2.1",
+ "@malept/cross-spawn-promise": "^1.1.0",
+ "debug": "^4.3.1",
+ "dir-compare": "^3.0.0",
+ "fs-extra": "^9.0.1",
+ "minimatch": "^3.0.4",
+ "plist": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@epic-web/invariant": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
+ "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
+ "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@malept/cross-spawn-promise": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
+ "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/malept"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
+ }
+ ],
+ "license": "Apache-2.0",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
+ "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.0",
+ "lodash": "^4.17.15",
+ "tmp-promise": "^3.0.2"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
+ "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "18.19.123",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.123.tgz",
+ "integrity": "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@types/plist": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
+ "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*",
+ "xmlbuilder": ">=11.0.1"
+ }
+ },
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/verror": {
+ "version": "1.10.11",
+ "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
+ "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.11",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/7zip-bin": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
+ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/app-builder-bin": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz",
+ "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/app-builder-lib": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz",
+ "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@develar/schema-utils": "~2.6.5",
+ "@electron/notarize": "2.2.1",
+ "@electron/osx-sign": "1.0.5",
+ "@electron/universal": "1.5.1",
+ "@malept/flatpak-bundler": "^0.4.0",
+ "@types/fs-extra": "9.0.13",
+ "async-exit-hook": "^2.0.1",
+ "bluebird-lst": "^1.0.9",
+ "builder-util": "24.13.1",
+ "builder-util-runtime": "9.2.4",
+ "chromium-pickle-js": "^0.2.0",
+ "debug": "^4.3.4",
+ "ejs": "^3.1.8",
+ "electron-publish": "24.13.1",
+ "form-data": "^4.0.0",
+ "fs-extra": "^10.1.0",
+ "hosted-git-info": "^4.1.0",
+ "is-ci": "^3.0.0",
+ "isbinaryfile": "^5.0.0",
+ "js-yaml": "^4.1.0",
+ "lazy-val": "^1.0.5",
+ "minimatch": "^5.1.1",
+ "read-config-file": "6.3.2",
+ "sanitize-filename": "^1.6.3",
+ "semver": "^7.3.8",
+ "tar": "^6.1.12",
+ "temp-file": "^3.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "dmg-builder": "24.13.3",
+ "electron-builder-squirrel-windows": "24.13.3"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "node_modules/archiver": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
+ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "async": "^3.2.4",
+ "buffer-crc32": "^0.2.1",
+ "readable-stream": "^3.6.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^2.2.0",
+ "zip-stream": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/archiver-utils/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-exit-hook": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
+ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bluebird-lst": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz",
+ "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "^3.5.5"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/boolean": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz",
+ "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/builder-util": {
+ "version": "24.13.1",
+ "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz",
+ "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.1.6",
+ "7zip-bin": "~5.2.0",
+ "app-builder-bin": "4.0.0",
+ "bluebird-lst": "^1.0.9",
+ "builder-util-runtime": "9.2.4",
+ "chalk": "^4.1.2",
+ "cross-spawn": "^7.0.3",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.1.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "is-ci": "^3.0.0",
+ "js-yaml": "^4.1.0",
+ "source-map-support": "^0.5.19",
+ "stat-mode": "^1.0.0",
+ "temp-file": "^3.4.0"
+ }
+ },
+ "node_modules/builder-util-runtime": {
+ "version": "9.2.4",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
+ "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "sax": "^1.2.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/builder-util/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/builder-util/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/builder-util/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chromium-pickle-js": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
+ "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/compare-version": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
+ "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/compress-commons": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
+ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer-crc32": "^0.2.13",
+ "crc32-stream": "^4.0.2",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "engines": [
+ "node >= 0.8"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/concat-stream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/concat-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/concat-stream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/concurrently": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
+ "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "date-fns": "^2.30.0",
+ "lodash": "^4.17.21",
+ "rxjs": "^7.8.1",
+ "shell-quote": "^1.8.1",
+ "spawn-command": "0.0.2",
+ "supports-color": "^8.1.1",
+ "tree-kill": "^1.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": "^14.13.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/config-file-ts": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz",
+ "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^10.3.10",
+ "typescript": "^5.3.3"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/crc": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+ "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "buffer": "^5.1.0"
+ }
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
+ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz",
+ "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@epic-web/invariant": "^1.0.0",
+ "cross-spawn": "^7.0.6"
+ },
+ "bin": {
+ "cross-env": "dist/bin/cross-env.js",
+ "cross-env-shell": "dist/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/dir-compare": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz",
+ "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal": "^1.0.0",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "node_modules/dir-compare/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/dir-compare/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/dmg-builder": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz",
+ "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "app-builder-lib": "24.13.3",
+ "builder-util": "24.13.1",
+ "builder-util-runtime": "9.2.4",
+ "fs-extra": "^10.1.0",
+ "iconv-lite": "^0.6.2",
+ "js-yaml": "^4.1.0"
+ },
+ "optionalDependencies": {
+ "dmg-license": "^1.0.11"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/dmg-license": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
+ "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "@types/plist": "^3.0.1",
+ "@types/verror": "^1.10.3",
+ "ajv": "^6.10.0",
+ "crc": "^3.8.0",
+ "iconv-corefoundation": "^1.1.7",
+ "plist": "^3.0.4",
+ "smart-buffer": "^4.0.2",
+ "verror": "^1.10.0"
+ },
+ "bin": {
+ "dmg-license": "bin/dmg-license.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
+ "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/dotenv-expand": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
+ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron": {
+ "version": "27.3.11",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-27.3.11.tgz",
+ "integrity": "sha512-E1SiyEoI8iW5LW/MigCr7tJuQe7+0105UjqY7FkmCD12e2O6vtUbQ0j05HaBh2YgvkcEVgvQ2A8suIq5b5m6Gw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/get": "^2.0.0",
+ "@types/node": "^18.11.18",
+ "extract-zip": "^2.0.1"
+ },
+ "bin": {
+ "electron": "cli.js"
+ },
+ "engines": {
+ "node": ">= 12.20.55"
+ }
+ },
+ "node_modules/electron-builder": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz",
+ "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "app-builder-lib": "24.13.3",
+ "builder-util": "24.13.1",
+ "builder-util-runtime": "9.2.4",
+ "chalk": "^4.1.2",
+ "dmg-builder": "24.13.3",
+ "fs-extra": "^10.1.0",
+ "is-ci": "^3.0.0",
+ "lazy-val": "^1.0.5",
+ "read-config-file": "6.3.2",
+ "simple-update-notifier": "2.0.0",
+ "yargs": "^17.6.2"
+ },
+ "bin": {
+ "electron-builder": "cli.js",
+ "install-app-deps": "install-app-deps.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz",
+ "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "app-builder-lib": "24.13.3",
+ "archiver": "^5.3.1",
+ "builder-util": "24.13.1",
+ "fs-extra": "^10.1.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-log": {
+ "version": "4.4.8",
+ "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.8.tgz",
+ "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==",
+ "license": "MIT"
+ },
+ "node_modules/electron-publish": {
+ "version": "24.13.1",
+ "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz",
+ "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/fs-extra": "^9.0.11",
+ "builder-util": "24.13.1",
+ "builder-util-runtime": "9.2.4",
+ "chalk": "^4.1.2",
+ "fs-extra": "^10.1.0",
+ "lazy-val": "^1.0.5",
+ "mime": "^2.5.2"
+ }
+ },
+ "node_modules/electron-publish/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-publish/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-publish/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-updater": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz",
+ "integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==",
+ "license": "MIT",
+ "dependencies": {
+ "builder-util-runtime": "9.3.1",
+ "fs-extra": "^10.1.0",
+ "js-yaml": "^4.1.0",
+ "lazy-val": "^1.0.5",
+ "lodash.escaperegexp": "^4.1.2",
+ "lodash.isequal": "^4.5.0",
+ "semver": "^7.6.3",
+ "tiny-typed-emitter": "^2.1.0"
+ }
+ },
+ "node_modules/electron-updater/node_modules/builder-util-runtime": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz",
+ "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "sax": "^1.2.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/electron-updater/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-updater/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-updater/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/electron-updater/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es6-error": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extsprintf": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
+ "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+ "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/global-agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "es6-error": "^4.1.1",
+ "matcher": "^3.0.0",
+ "roarr": "^2.15.3",
+ "semver": "^7.3.2",
+ "serialize-error": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/global-agent/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/got": {
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/iconv-corefoundation": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
+ "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "node-addon-api": "^1.6.3"
+ },
+ "engines": {
+ "node": "^8.11.2 || >=10"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+ "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ci-info": "^3.2.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/isbinaryfile": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz",
+ "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/lazy-val": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
+ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
+ "license": "MIT"
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.escaperegexp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+ "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.union": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/matcher": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "1.4.5-lts.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
+ "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
+ "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.0.0",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.4",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
+ "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/plist": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
+ "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
+ "base64-js": "^1.5.1",
+ "xmlbuilder": "^15.1.1"
+ },
+ "engines": {
+ "node": ">=10.4.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-config-file": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
+ "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "config-file-ts": "^0.2.4",
+ "dotenv": "^9.0.2",
+ "dotenv-expand": "^5.1.0",
+ "js-yaml": "^4.1.0",
+ "json5": "^2.2.0",
+ "lazy-val": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/responselike": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/roarr": {
+ "version": "2.15.4",
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "detect-node": "^2.0.4",
+ "globalthis": "^1.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "semver-compare": "^1.0.0",
+ "sprintf-js": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/sanitize-filename": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+ "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+ "dev": true,
+ "license": "WTFPL OR ISC",
+ "dependencies": {
+ "truncate-utf8-bytes": "^1.0.0"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+ "license": "ISC"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/serialize-error": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "type-fest": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/spawn-command": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
+ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
+ "dev": true
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/stat-mode": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
+ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sumchecker": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
+ "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/temp-file": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
+ "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-exit-hook": "^2.0.1",
+ "fs-extra": "^10.0.0"
+ }
+ },
+ "node_modules/temp-file/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/temp-file/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/temp-file/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/tiny-typed-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
+ "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
+ "license": "MIT"
+ },
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/tmp-promise": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
+ "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tmp": "^0.2.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/truncate-utf8-bytes": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+ "dev": true,
+ "license": "WTFPL",
+ "dependencies": {
+ "utf8-byte-length": "^1.0.1"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-fest": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/utf8-byte-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)"
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/verror": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
+ "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+ "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/zip-stream": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
+ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "archiver-utils": "^3.0.4",
+ "compress-commons": "^4.1.2",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/zip-stream/node_modules/archiver-utils": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
+ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.2.3",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ }
+ }
+}
diff --git a/synapse-electron/package.json b/synapse-electron/package.json
new file mode 100644
index 000000000..03f8272ae
--- /dev/null
+++ b/synapse-electron/package.json
@@ -0,0 +1,99 @@
+{
+ "name": "synapse-desktop-client",
+ "version": "0.1.0",
+ "description": "Synapse Desktop Client - Electron version",
+ "main": "main.js",
+ "scripts": {
+ "start": "electron .",
+ "dev": "electron .",
+ "build": "electron-builder",
+ "dist": "electron-builder",
+ "dist:win": "electron-builder --win",
+ "dist:mac": "electron-builder --mac",
+ "dist:linux": "electron-builder --linux"
+ },
+ "build": {
+ "appId": "com.sagebase.synapse-desktop-client",
+ "productName": "Synapse Desktop Client",
+ "directories": {
+ "output": "dist"
+ },
+ "files": [
+ "**/*",
+ "!backend/__pycache__",
+ "!backend/*.pyc",
+ "!backend/venv",
+ "!backend/build",
+ "!backend/*.spec"
+ ],
+ "extraResources": [
+ {
+ "from": "backend/dist",
+ "to": "backend",
+ "filter": [
+ "**/*"
+ ]
+ }
+ ],
+ "win": {
+ "target": [
+ {
+ "target": "nsis",
+ "arch": [
+ "x64"
+ ]
+ }
+ ],
+ "icon": "assets/icon.ico",
+ "verifyUpdateCodeSignature": false,
+ "artifactName": "${productName} Setup ${version}-${os}.${ext}"
+ },
+ "mac": {
+ "target": [
+ {
+ "target": "dmg",
+ "arch": [
+ "x64",
+ "arm64"
+ ]
+ }
+ ],
+ "icon": "assets/icon.icns",
+ "artifactName": "${productName}-${version}-${arch}-${os}.${ext}"
+ },
+ "linux": {
+ "target": [
+ {
+ "target": "AppImage",
+ "arch": [
+ "x64"
+ ]
+ }
+ ],
+ "icon": "assets/icon.png"
+ },
+ "nsis": {
+ "oneClick": false,
+ "allowToChangeInstallationDirectory": true,
+ "createDesktopShortcut": true,
+ "createStartMenuShortcut": true
+ }
+ },
+ "devDependencies": {
+ "concurrently": "^8.2.2",
+ "cross-env": "^10.0.0",
+ "electron": "^27.0.0",
+ "electron-builder": "^24.6.4"
+ },
+ "dependencies": {
+ "axios": "^1.5.0",
+ "cors": "^2.8.5",
+ "electron-log": "^4.4.8",
+ "electron-updater": "^6.1.4",
+ "express": "^4.18.2",
+ "multer": "^1.4.5-lts.1",
+ "ws": "^8.14.2"
+ },
+ "author": "Sage Bionetworks",
+ "license": "Apache-2.0"
+}
diff --git a/synapse-electron/preload.js b/synapse-electron/preload.js
new file mode 100644
index 000000000..378b43a43
--- /dev/null
+++ b/synapse-electron/preload.js
@@ -0,0 +1,39 @@
+const { contextBridge, ipcRenderer } = require('electron');
+
+// Expose protected methods that allow the renderer process to use
+// the ipcRenderer without exposing the entire object
+contextBridge.exposeInMainWorld('electronAPI', {
+ // Authentication
+ login: (credentials) => ipcRenderer.invoke('synapse-login', credentials),
+ logout: () => ipcRenderer.invoke('synapse-logout'),
+ getProfiles: () => ipcRenderer.invoke('synapse-get-profiles'),
+
+ // File operations
+ uploadFile: (uploadData) => ipcRenderer.invoke('synapse-upload', uploadData),
+ downloadFile: (downloadData) => ipcRenderer.invoke('synapse-download', downloadData),
+
+ // Bulk operations
+ enumerate: (containerData) => ipcRenderer.invoke('synapse-enumerate', containerData),
+ bulkDownload: (bulkData) => ipcRenderer.invoke('synapse-bulk-download', bulkData),
+ bulkUpload: (bulkData) => ipcRenderer.invoke('synapse-bulk-upload', bulkData),
+ scanDirectory: (scanData) => ipcRenderer.invoke('synapse-scan-directory', scanData),
+
+ // File dialogs
+ showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
+ showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
+
+ // External links
+ openExternal: (url) => ipcRenderer.invoke('open-external', url),
+
+ // App info
+ getAppVersion: () => ipcRenderer.invoke('get-app-version'),
+ getHomeDirectory: () => ipcRenderer.invoke('get-home-directory'),
+
+ // WebSocket events
+ onWebSocketMessage: (callback) => ipcRenderer.on('websocket-message', callback),
+ removeWebSocketListener: (callback) => ipcRenderer.removeListener('websocket-message', callback),
+
+ // Window events
+ onWindowClose: (callback) => ipcRenderer.on('window-close', callback),
+ removeWindowCloseListener: (callback) => ipcRenderer.removeListener('window-close', callback)
+});
diff --git a/synapse-electron/src/app.js b/synapse-electron/src/app.js
new file mode 100644
index 000000000..ad16d3026
--- /dev/null
+++ b/synapse-electron/src/app.js
@@ -0,0 +1,2105 @@
+// Synapse Desktop Client - Electron Frontend Application
+class SynapseDesktopClient {
+ constructor() {
+ this.isLoggedIn = false;
+ this.currentUser = null;
+ this.activeTab = 'download';
+ this.websocketConnected = false;
+ this.containerItems = [];
+ this.selectedItems = new Set();
+
+ // Upload-specific properties
+ this.uploadFileItems = [];
+ this.selectedUploadFiles = new Set();
+
+ // Auto-scroll related properties
+ this.autoScrollEnabled = true;
+ this.lastScrollTime = 0;
+ this.scrollThrottleDelay = 50; // Minimum ms between scroll operations
+ this.pendingScroll = false;
+
+ this.initializeApp();
+ }
+
+ async initializeApp() {
+ try {
+ await this.setupEventListeners();
+ await this.loadAppVersion();
+ await this.loadProfiles();
+ await this.setupWebSocketListener();
+
+ // Initialize auto-scroll UI
+ this.updateAutoScrollUI();
+ this.updateLogCount();
+
+ // Start polling for log messages
+ this.startLogPolling();
+
+ // Set default download location
+ const homeResult = await window.electronAPI.getHomeDirectory();
+ let defaultDownloadPath = 'Downloads'; // fallback
+
+ if (homeResult.success && homeResult.data.downloads_directory) {
+ defaultDownloadPath = homeResult.data.downloads_directory;
+ } else {
+ console.error('Error getting home directory:', homeResult.error);
+ }
+
+ const downloadLocationEl = document.getElementById('download-location');
+ const bulkDownloadLocationEl = document.getElementById('bulk-download-location');
+
+ if (downloadLocationEl) {
+ downloadLocationEl.value = defaultDownloadPath;
+ }
+ if (bulkDownloadLocationEl) {
+ bulkDownloadLocationEl.value = defaultDownloadPath;
+ }
+
+ // Initialize button states based on current form values
+ this.updateUploadButtonState();
+ this.updateDownloadButtonState();
+ this.updateBulkUploadButtonState();
+ this.updateBulkDownloadButtonState();
+
+ console.log('Synapse Desktop Client initialized');
+ } catch (error) {
+ console.error('Error in initializeApp:', error);
+ }
+ }
+
+ async loadAppVersion() {
+ try {
+ const version = await window.electronAPI.getAppVersion();
+ document.getElementById('app-version').textContent = `v${version}`;
+ } catch (error) {
+ console.error('Error loading app version:', error);
+ }
+ }
+
+ setupEventListeners() {
+ // Cleanup on window close
+ window.addEventListener('beforeunload', () => {
+ this.stopLogPolling();
+ });
+
+ // Login mode toggle
+ document.querySelectorAll('input[name="loginMode"]').forEach(radio => {
+ radio.addEventListener('change', (e) => {
+ this.toggleLoginMode(e.target.value);
+ });
+ });
+
+ // Login button
+ document.getElementById('login-btn').addEventListener('click', () => {
+ this.handleLogin();
+ });
+
+ // Logout button
+ document.getElementById('logout-btn').addEventListener('click', () => {
+ this.handleLogout();
+ });
+
+ // Profile selection
+ document.getElementById('profile').addEventListener('change', (e) => {
+ this.updateProfileInfo(e.target.value);
+ });
+
+ // Tab navigation
+ document.querySelectorAll('.tab-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ this.switchTab(e.currentTarget.dataset.tab);
+ });
+ });
+
+ // File browser buttons
+ document.getElementById('browse-download-location').addEventListener('click', () => {
+ this.browseDirectory('download-location');
+ });
+
+ document.getElementById('browse-upload-file').addEventListener('click', () => {
+ this.browseFile('upload-file');
+ });
+
+ document.getElementById('browse-bulk-download-location').addEventListener('click', () => {
+ this.browseDirectory('bulk-download-location');
+ });
+
+ document.getElementById('browse-bulk-upload-location').addEventListener('click', () => {
+ this.browseDirectory('bulk-upload-location');
+ });
+
+ // Upload mode toggle
+ document.querySelectorAll('input[name="uploadMode"]').forEach(radio => {
+ radio.addEventListener('change', (e) => {
+ this.toggleUploadMode(e.target.value);
+ });
+ });
+
+ // Operation buttons
+ document.getElementById('download-btn').addEventListener('click', () => {
+ this.handleDownload();
+ });
+
+ document.getElementById('upload-btn').addEventListener('click', () => {
+ this.handleUpload();
+ });
+
+ document.getElementById('enumerate-btn').addEventListener('click', () => {
+ this.handleEnumerate();
+ });
+
+ document.getElementById('scan-upload-btn').addEventListener('click', () => {
+ this.handleScanUploadDirectory();
+ });
+
+ document.getElementById('bulk-download-btn').addEventListener('click', () => {
+ this.handleBulkDownload();
+ });
+
+ document.getElementById('bulk-upload-btn').addEventListener('click', () => {
+ this.handleBulkUpload();
+ });
+
+ // Selection controls
+ document.getElementById('select-all-btn').addEventListener('click', () => {
+ this.selectAllItems(true);
+ });
+
+ document.getElementById('deselect-all-btn').addEventListener('click', () => {
+ this.selectAllItems(false);
+ });
+
+ document.getElementById('expand-all-btn').addEventListener('click', () => {
+ this.expandAllFolders();
+ });
+
+ document.getElementById('collapse-all-btn').addEventListener('click', () => {
+ this.collapseAllFolders();
+ });
+
+ // Upload selection controls
+ document.getElementById('select-all-upload-btn').addEventListener('click', () => {
+ this.selectAllUploadFiles(true);
+ });
+
+ document.getElementById('deselect-all-upload-btn').addEventListener('click', () => {
+ this.selectAllUploadFiles(false);
+ });
+
+ document.getElementById('expand-all-upload-btn').addEventListener('click', () => {
+ this.expandAllUploadFolders();
+ });
+
+ document.getElementById('collapse-all-upload-btn').addEventListener('click', () => {
+ this.collapseAllUploadFolders();
+ });
+
+ // Clear output
+ document.getElementById('clear-output-btn').addEventListener('click', () => {
+ this.clearOutput();
+ });
+
+ // Auto-scroll toggle
+ document.getElementById('auto-scroll-toggle').addEventListener('click', () => {
+ this.toggleAutoScroll();
+ });
+
+ // Scroll to bottom
+ document.getElementById('scroll-to-bottom-btn').addEventListener('click', () => {
+ this.scrollToBottom();
+ });
+
+ // Detect manual scrolling to temporarily disable auto-scroll
+ document.getElementById('output-log').addEventListener('scroll', (e) => {
+ this.handleManualScroll(e);
+ });
+
+ // File input change for auto-naming
+ document.getElementById('upload-file').addEventListener('change', (e) => {
+ if (e.target.value && !document.getElementById('upload-name').value) {
+ const fileName = e.target.value.split('\\').pop().split('/').pop();
+ document.getElementById('upload-name').value = fileName;
+ }
+ // Re-enable upload button if form is now valid
+ this.updateUploadButtonState();
+ });
+
+ // Download input changes
+ document.getElementById('download-id').addEventListener('input', () => {
+ this.updateDownloadButtonState();
+ });
+
+ document.getElementById('download-location').addEventListener('change', () => {
+ this.updateDownloadButtonState();
+ });
+
+ // Upload mode and ID input changes
+ document.querySelectorAll('input[name="uploadMode"]').forEach(radio => {
+ radio.addEventListener('change', () => {
+ this.updateUploadButtonState();
+ });
+ });
+
+ document.getElementById('parent-id').addEventListener('input', () => {
+ this.updateUploadButtonState();
+ });
+
+ document.getElementById('entity-id').addEventListener('input', () => {
+ this.updateUploadButtonState();
+ });
+
+ // Bulk upload directory change
+ document.getElementById('bulk-upload-location').addEventListener('change', () => {
+ this.handleScanUploadDirectory();
+ });
+
+ // Bulk operation form field changes
+ document.getElementById('bulk-parent-id').addEventListener('input', () => {
+ this.updateBulkUploadButtonState();
+ });
+
+ document.getElementById('bulk-download-location').addEventListener('change', () => {
+ this.updateBulkDownloadButtonState();
+ });
+ }
+
+ setupWebSocketListener() {
+ window.electronAPI.onWebSocketMessage((event, message) => {
+ this.handleWebSocketMessage(message);
+ });
+ }
+
+ startLogPolling() {
+ // Poll for log messages every 500ms
+ this.logPollingInterval = setInterval(async () => {
+ try {
+ const response = await fetch('http://localhost:8000/logs/poll');
+ if (response.ok) {
+ const result = await response.json();
+ if (result.success && result.messages && result.messages.length > 0) {
+ // Process each log message
+ result.messages.forEach(message => {
+ this.handleLogMessage(message);
+ });
+ }
+ }
+ } catch (error) {
+ // Silently ignore polling errors to avoid spam
+ // Only log if we're having connection issues
+ if (error.message && !error.message.includes('Failed to fetch')) {
+ console.debug('Log polling error:', error);
+ }
+ }
+ }, 500);
+ }
+
+ stopLogPolling() {
+ if (this.logPollingInterval) {
+ clearInterval(this.logPollingInterval);
+ this.logPollingInterval = null;
+ }
+ }
+
+ handleWebSocketMessage(message) {
+ console.log('WebSocket message received:', message);
+ switch (message.type) {
+ case 'log':
+ this.handleLogMessage(message);
+ break;
+ case 'complete':
+ console.log('Handling completion message:', message);
+ this.handleOperationComplete(message.operation, message.success, message.data);
+ break;
+ case 'connection_status':
+ this.updateConnectionStatus(message.connected);
+ break;
+ case 'scroll_command':
+ this.handleScrollCommand(message);
+ break;
+ default:
+ console.log('Unknown WebSocket message:', message);
+ }
+ }
+
+ handleLogMessage(logMessage) {
+ // Display in the UI log with enhanced formatting
+ this.logMessageAdvanced(logMessage);
+
+ // Auto-scroll if enabled and requested
+ if (logMessage.auto_scroll) {
+ this.autoScrollIfEnabled();
+ }
+
+ // Also log to browser console with more details
+ const timestamp = logMessage.timestamp ?
+ new Date(logMessage.timestamp * 1000).toISOString() :
+ new Date().toISOString();
+ const consoleMessage = `[${timestamp}] [${logMessage.logger_name || 'backend'}] ${logMessage.message}`;
+
+ switch (logMessage.level) {
+ case 'error':
+ case 'critical':
+ console.error(consoleMessage);
+ break;
+ case 'warning':
+ case 'warn':
+ console.warn(consoleMessage);
+ break;
+ case 'debug':
+ console.debug(consoleMessage);
+ break;
+ case 'info':
+ default:
+ console.info(consoleMessage);
+ break;
+ }
+
+ // For synapse client logs, add special formatting
+ if (logMessage.logger_name && logMessage.logger_name.includes('synapse')) {
+ console.group(`🔬 Synapse Client Log [${logMessage.level.toUpperCase()}]`);
+ console.log(`Logger: ${logMessage.logger_name}`);
+ console.log(`Message: ${logMessage.message}`);
+ console.log(`Timestamp: ${timestamp}`);
+ if (logMessage.operation) {
+ console.log(`Operation: ${logMessage.operation}`);
+ }
+ console.groupEnd();
+ }
+ }
+
+ updateConnectionStatus(connected) {
+ this.websocketConnected = connected;
+ const statusElement = document.getElementById('connection-status');
+ const icon = statusElement.querySelector('i');
+
+ if (connected) {
+ icon.className = 'fas fa-circle status-connected';
+ statusElement.lastChild.textContent = 'Connected';
+ } else {
+ icon.className = 'fas fa-circle status-disconnected';
+ statusElement.lastChild.textContent = 'Disconnected';
+ }
+ }
+
+ async loadProfiles() {
+ try {
+ const result = await window.electronAPI.getProfiles();
+ if (result.success) {
+ this.populateProfiles(result.data.profiles);
+ this.updateLoginModeAvailability(result.data.profiles.length > 0);
+ }
+ } catch (error) {
+ console.error('Error loading profiles:', error);
+ this.updateLoginModeAvailability(false);
+ }
+ }
+
+ populateProfiles(profiles) {
+ const profileSelect = document.getElementById('profile');
+ profileSelect.innerHTML = '';
+
+ profiles.forEach(profile => {
+ const option = document.createElement('option');
+ option.value = profile.name;
+ option.textContent = profile.display_name || profile.name;
+ profileSelect.appendChild(option);
+ });
+ }
+
+ updateLoginModeAvailability(hasProfiles) {
+ const configMode = document.getElementById('config-mode');
+ const manualMode = document.getElementById('manual-mode');
+
+ if (!hasProfiles) {
+ configMode.disabled = true;
+ manualMode.checked = true;
+ this.toggleLoginMode('manual');
+ } else {
+ configMode.disabled = false;
+ }
+ }
+
+ updateProfileInfo(profileName) {
+ const infoElement = document.getElementById('profile-info');
+ if (profileName) {
+ infoElement.textContent = `Using profile: ${profileName}`;
+ } else {
+ infoElement.textContent = '';
+ }
+ }
+
+ toggleLoginMode(mode) {
+ const manualForm = document.getElementById('manual-login');
+ const configForm = document.getElementById('config-login');
+
+ if (mode === 'manual') {
+ manualForm.classList.add('active');
+ configForm.classList.remove('active');
+ } else {
+ manualForm.classList.remove('active');
+ configForm.classList.add('active');
+ }
+ }
+
+ toggleUploadMode(mode) {
+ const parentIdGroup = document.getElementById('parent-id-group');
+ const entityIdGroup = document.getElementById('entity-id-group');
+
+ if (mode === 'new') {
+ parentIdGroup.style.display = 'block';
+ entityIdGroup.style.display = 'none';
+ document.getElementById('entity-id').value = '';
+ } else {
+ parentIdGroup.style.display = 'none';
+ entityIdGroup.style.display = 'block';
+ document.getElementById('parent-id').value = '';
+ }
+ }
+
+ switchTab(tabName) {
+ if (!tabName) {
+ console.error('switchTab called with invalid tabName:', tabName);
+ return;
+ }
+
+ // Update tab buttons
+ document.querySelectorAll('.tab-btn').forEach(btn => {
+ btn.classList.remove('active');
+ });
+
+ const activeTabBtn = document.querySelector(`[data-tab="${tabName}"]`);
+ if (activeTabBtn) {
+ activeTabBtn.classList.add('active');
+ } else {
+ console.error(`No tab button found with data-tab="${tabName}"`);
+ }
+
+ // Update tab panels
+ document.querySelectorAll('.tab-panel').forEach(panel => {
+ panel.classList.remove('active');
+ });
+
+ const activeTabPanel = document.getElementById(`${tabName}-tab`);
+ if (activeTabPanel) {
+ activeTabPanel.classList.add('active');
+ } else {
+ console.error(`No tab panel found with id="${tabName}-tab"`);
+ }
+
+ this.activeTab = tabName;
+ }
+
+ async handleLogin() {
+ const loginBtn = document.getElementById('login-btn');
+ const statusDiv = document.getElementById('login-status');
+
+ loginBtn.disabled = true;
+ loginBtn.innerHTML = ' Logging in...';
+
+ try {
+ const mode = document.querySelector('input[name="loginMode"]:checked').value;
+ let credentials = { mode };
+
+ if (mode === 'manual') {
+ credentials.username = document.getElementById('username').value.trim();
+ credentials.token = document.getElementById('token').value.trim();
+
+ if (!credentials.username || !credentials.token) {
+ throw new Error('Username and token are required');
+ }
+ } else {
+ credentials.profile = document.getElementById('profile').value;
+
+ if (!credentials.profile) {
+ throw new Error('Please select a profile');
+ }
+ }
+
+ const result = await window.electronAPI.login(credentials);
+
+ if (result.success) {
+ this.handleLoginSuccess(result.data);
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ this.showStatus('login-status', error.message, 'error');
+ } finally {
+ loginBtn.disabled = false;
+ loginBtn.innerHTML = ' Login';
+ }
+ }
+
+ handleLoginSuccess(userData) {
+ this.isLoggedIn = true;
+ this.currentUser = userData;
+
+ // Hide login section and show main app
+ document.getElementById('login-section').style.display = 'none';
+ document.getElementById('main-section').style.display = 'flex';
+
+ // Update user info
+ document.getElementById('user-name').textContent = userData.username || userData.name || 'User';
+
+ this.logMessage(`Successfully logged in as ${userData.username || userData.name}`, false);
+ this.setStatus('Logged in successfully');
+ }
+
+ async handleLogout() {
+ try {
+ await window.electronAPI.logout();
+
+ this.isLoggedIn = false;
+ this.currentUser = null;
+
+ // Show login section and hide main app
+ document.getElementById('login-section').style.display = 'flex';
+ document.getElementById('main-section').style.display = 'none';
+
+ // Clear form data
+ document.getElementById('username').value = '';
+ document.getElementById('token').value = '';
+ document.getElementById('profile').value = '';
+
+ // Clear status
+ document.getElementById('login-status').classList.remove('success', 'error', 'info');
+ document.getElementById('login-status').style.display = 'none';
+
+ this.logMessage('Logged out successfully', false);
+ this.setStatus('Ready');
+ } catch (error) {
+ console.error('Logout error:', error);
+ this.logMessage(`Logout error: ${error.message}`, true);
+ }
+ }
+
+ async browseDirectory(inputId) {
+ try {
+ const result = await window.electronAPI.showOpenDialog({
+ properties: ['openDirectory'],
+ defaultPath: document.getElementById(inputId).value || ''
+ });
+
+ if (!result.canceled && result.filePaths.length > 0) {
+ document.getElementById(inputId).value = result.filePaths[0];
+
+ // Trigger scan for bulk upload if it's the upload location
+ if (inputId === 'bulk-upload-location') {
+ this.handleScanUploadDirectory();
+ }
+ }
+ } catch (error) {
+ console.error('Error browsing directory:', error);
+ this.logMessage(`Error selecting directory: ${error.message}`, true);
+ }
+ }
+
+ async browseFile(inputId) {
+ try {
+ const result = await window.electronAPI.showOpenDialog({
+ properties: ['openFile'],
+ defaultPath: document.getElementById(inputId).value || ''
+ });
+
+ if (!result.canceled && result.filePaths.length > 0) {
+ document.getElementById(inputId).value = result.filePaths[0];
+
+ // Auto-populate upload name if empty
+ if (inputId === 'upload-file' && !document.getElementById('upload-name').value) {
+ const fileName = result.filePaths[0].split('\\').pop().split('/').pop();
+ document.getElementById('upload-name').value = fileName;
+ }
+ }
+ } catch (error) {
+ console.error('Error browsing file:', error);
+ this.logMessage(`Error selecting file: ${error.message}`, true);
+ }
+ }
+
+ async handleDownload() {
+ const synapseId = document.getElementById('download-id').value.trim();
+ const version = document.getElementById('download-version').value.trim();
+ const downloadPath = document.getElementById('download-location').value.trim();
+
+ if (!synapseId) {
+ this.logMessage('Synapse ID is required for download', true);
+ return;
+ }
+
+ if (!downloadPath) {
+ this.logMessage('Download location is required', true);
+ return;
+ }
+
+ try {
+ const result = await window.electronAPI.downloadFile({
+ synapse_id: synapseId,
+ version: version || null,
+ download_path: downloadPath
+ });
+
+ if (result.success) {
+ this.logMessage(`Download initiated for ${synapseId}`, false);
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ this.logMessage(`Download error: ${error.message}`, true);}
+ }
+
+ async handleUpload() {
+ const filePath = document.getElementById('upload-file').value.trim();
+ const mode = document.querySelector('input[name="uploadMode"]:checked').value;
+ const parentId = document.getElementById('parent-id').value.trim();
+ const entityId = document.getElementById('entity-id').value.trim();
+ const name = document.getElementById('upload-name').value.trim();
+
+ if (!filePath) {
+ this.logMessage('File path is required for upload', true);
+ return;
+ }
+
+ if (mode === 'new' && !parentId) {
+ this.logMessage('Parent ID is required for new file upload', true);
+ return;
+ }
+
+ if (mode === 'update' && !entityId) {
+ this.logMessage('Entity ID is required for file update', true);
+ return;
+ }
+
+ try {
+ const result = await window.electronAPI.uploadFile({
+ file_path: filePath,
+ mode: mode,
+ parent_id: parentId || null,
+ entity_id: entityId || null,
+ name: name || null
+ });
+
+ if (result.success) {
+ this.logMessage(`Upload initiated for ${filePath}`, false);
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ this.logMessage(`Upload error: ${error.message}`, true);}
+ }
+
+ async handleEnumerate() {
+ const containerId = document.getElementById('container-id').value.trim();
+ const recursive = document.getElementById('recursive').checked;
+
+ if (!containerId) {
+ this.logMessage('Container ID is required for enumeration', true);
+ return;
+ }
+
+ try {
+ const result = await window.electronAPI.enumerate({
+ container_id: containerId,
+ recursive: recursive
+ });
+
+ if (result.success) {
+ this.displayContainerContents(result.data.items);
+ this.logMessage(`Found ${result.data.items.length} items in container`, false);
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ this.logMessage(`Enumeration error: ${error.message}`, true);
+ } finally {}
+ }
+
+ displayContainerContents(items) {
+ this.containerItems = items;
+ this.selectedItems.clear();
+
+ const contentsDiv = document.getElementById('container-contents');
+ const treeDiv = document.getElementById('items-tree');
+
+ treeDiv.innerHTML = '';
+
+ // Build hierarchical structure
+ const { rootItems, parentMap } = this.buildHierarchy(items);
+
+ // Use JSTree for better tree functionality
+ this.initializeJSTree(treeDiv, rootItems, parentMap);
+
+ contentsDiv.style.display = 'block';
+
+ // Update status
+ const folderCount = items.filter(item => {
+ const itemType = item.type || item.item_type || 'file';
+ return itemType === 'folder';
+ }).length;
+ const fileCount = items.length - folderCount;
+ this.logMessage(`Loaded ${items.length} items (${folderCount} folders, ${fileCount} files)`, false);
+ }
+
+ initializeJSTree(container, rootItems, parentMap) {
+ // Clear any existing JSTree
+ if ($.fn.jstree && $(container).jstree) {
+ $(container).jstree('destroy');
+ }
+
+ // Build JSTree data structure
+ const treeData = this.buildJSTreeData(rootItems, parentMap);
+
+ // Initialize JSTree with enhanced features
+ $(container).jstree({
+ 'core': {
+ 'data': treeData,
+ 'themes': {
+ 'name': 'default',
+ 'responsive': true,
+ 'icons': true
+ },
+ 'check_callback': true
+ },
+ 'checkbox': {
+ 'three_state': true, // Enable three-state checkboxes (checked, unchecked, indeterminate)
+ 'whole_node': true, // Click anywhere on the node to check/uncheck
+ 'tie_selection': false, // Don't tie selection to checkbox state
+ 'cascade': 'up+down' // Cascade checkbox state up and down the tree
+ },
+ 'types': {
+ 'folder': {
+ 'icon': 'fas fa-folder',
+ 'a_attr': { 'class': 'tree-folder' }
+ },
+ 'file': {
+ 'icon': 'fas fa-file',
+ 'a_attr': { 'class': 'tree-file' }
+ }
+ },
+ 'plugins': ['checkbox', 'types', 'wholerow']
+ })
+ .on('check_node.jstree uncheck_node.jstree', (e, data) => {
+ this.handleJSTreeSelection(data);
+ })
+ .on('ready.jstree', () => {
+ // Expand first level by default
+ $(container).jstree('open_all', null, 1);
+ this.updateSelectionCount();
+ });
+ }
+
+ buildJSTreeData(items, parentMap, parentPath = '') {
+ const treeData = [];
+
+ items.forEach(item => {
+ const hasChildren = parentMap[item.id] && parentMap[item.id].length > 0;
+ const currentPath = parentPath ? `${parentPath}/${item.name}` : item.name;
+ const sizeStr = this.formatFileSize(item.size);
+ const itemType = item.type || item.item_type || 'file';
+
+ const nodeData = {
+ 'id': item.id,
+ 'text': `${item.name} (${itemType}${sizeStr ? ', ' + sizeStr : ''})`,
+ 'type': itemType,
+ 'data': {
+ 'item': item,
+ 'path': currentPath,
+ 'size': item.size
+ },
+ 'state': {
+ 'opened': false
+ }
+ };
+
+ if (hasChildren) {
+ nodeData.children = this.buildJSTreeData(parentMap[item.id], parentMap, currentPath);
+ }
+
+ treeData.push(nodeData);
+ });
+
+ return treeData;
+ }
+
+ handleJSTreeSelection(data) {
+ console.log('handleJSTreeSelection called');
+ const treeDiv = document.getElementById('items-tree');
+
+ // Get all checked nodes to rebuild the selection set
+ if ($.fn.jstree && $(treeDiv).jstree) {
+ const checkedNodes = $(treeDiv).jstree('get_checked', true);
+ this.selectedItems.clear();
+
+ checkedNodes.forEach(node => {
+ if (node.data && node.data.item) {
+ this.selectedItems.add(node.data.item.id);
+ }
+ });
+
+ console.log(`handleJSTreeSelection: ${this.selectedItems.size} items selected`);
+ }
+
+ this.updateSelectionCount();
+ }
+
+ buildHierarchy(items) {
+ const parentMap = {};
+ const rootItems = [];
+ const allItemIds = new Set(items.map(item => item.id));
+
+ // Build parent-child relationships
+ items.forEach(item => {
+ if (item.parent_id) {
+ if (!parentMap[item.parent_id]) {
+ parentMap[item.parent_id] = [];
+ }
+ parentMap[item.parent_id].push(item);
+ } else {
+ rootItems.push(item);
+ }
+ });
+
+ // Handle orphaned items (parent not in current item set)
+ if (rootItems.length === 0 && items.length > 0) {
+ items.forEach(item => {
+ if (item.parent_id && !allItemIds.has(item.parent_id)) {
+ rootItems.push(item);
+ }
+ });
+ }
+
+ // Sort items: folders first, then alphabetically
+ rootItems.sort((a, b) => {
+ const aType = a.type || a.item_type || 'file';
+ const bType = b.type || b.item_type || 'file';
+ if (aType !== bType) {
+ return aType === 'folder' ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ });
+
+ Object.values(parentMap).forEach(children => {
+ children.sort((a, b) => {
+ const aType = a.type || a.item_type || 'file';
+ const bType = b.type || b.item_type || 'file';
+ if (aType !== bType) {
+ return aType === 'folder' ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ });
+ });
+
+ return { rootItems, parentMap };
+ }
+
+ populateTreeLevel(container, items, parentMap, level, parentPath = '') {
+ items.forEach(item => {
+ const hasChildren = parentMap[item.id] && parentMap[item.id].length > 0;
+ const currentPath = parentPath ? `${parentPath}/${item.name}` : item.name;
+
+ // Create tree item element
+ const itemDiv = this.createTreeItem(item, level, hasChildren, currentPath);
+ container.appendChild(itemDiv);
+
+ // Create children container
+ if (hasChildren) {
+ const childrenDiv = document.createElement('div');
+ childrenDiv.className = 'tree-children';
+ childrenDiv.id = `children-${item.id}`;
+
+ this.populateTreeLevel(
+ childrenDiv,
+ parentMap[item.id],
+ parentMap,
+ level + 1,
+ currentPath
+ );
+
+ container.appendChild(childrenDiv);
+ }
+ });
+ }
+
+ createTreeItem(item, level, hasChildren, currentPath) {
+ const itemDiv = document.createElement('div');
+ itemDiv.className = `tree-item level-${level}`;
+ itemDiv.dataset.itemId = item.id;
+ itemDiv.dataset.level = level;
+ itemDiv.style.setProperty('--indent-level', level);
+
+ // Format size for display
+ const sizeStr = this.formatFileSize(item.size);
+ const itemType = item.type || item.item_type || 'file';
+
+ itemDiv.innerHTML = `
+
+
+
+
+ ${this.escapeHtml(item.name)}
+ ${itemType}
+ ${sizeStr}
+ ${currentPath}
+
+ `;
+
+ // Add checkbox event listener
+ const checkbox = itemDiv.querySelector('input[type="checkbox"]');
+ checkbox.addEventListener('change', (e) => {
+ this.handleItemSelection(e, item, itemDiv);
+ });
+
+ // Add context menu support
+ itemDiv.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ this.showTreeItemContextMenu(e, item, itemDiv);
+ });
+
+ return itemDiv;
+ }
+
+ toggleTreeNode(itemId) {
+ const toggleButton = document.querySelector(`.tree-item[data-item-id="${itemId}"] .tree-toggle`);
+ const childrenDiv = document.getElementById(`children-${itemId}`);
+
+ if (toggleButton && childrenDiv) {
+ const isExpanded = toggleButton.classList.contains('expanded');
+
+ if (isExpanded) {
+ toggleButton.classList.remove('expanded');
+ toggleButton.classList.add('collapsed');
+ childrenDiv.style.display = 'none';
+ } else {
+ toggleButton.classList.remove('collapsed');
+ toggleButton.classList.add('expanded');
+ childrenDiv.style.display = 'block';
+ }
+ }
+ }
+
+ handleItemSelection(event, item, itemDiv) {
+ if (event.target.checked) {
+ this.selectedItems.add(item.id);
+ itemDiv.classList.add('selected');
+ } else {
+ this.selectedItems.delete(item.id);
+ itemDiv.classList.remove('selected');
+ }
+
+ // Update selection count display
+ this.updateSelectionCount();
+ }
+
+ updateSelectionCount() {
+ const count = this.selectedItems.size;
+ console.log(`updateSelectionCount called: ${count} items selected`);
+
+ // Calculate stats for selected items
+ const selectedItemsData = this.containerItems.filter(item =>
+ this.selectedItems.has(item.id)
+ );
+
+ const fileCount = selectedItemsData.filter(item => {
+ const itemType = item.type || item.item_type || 'file';
+ return itemType === 'file';
+ }).length;
+
+ const folderCount = selectedItemsData.filter(item => {
+ const itemType = item.type || item.item_type || 'file';
+ return itemType === 'folder';
+ }).length;
+
+ const totalSize = selectedItemsData.reduce((sum, item) => {
+ return sum + (item.size || 0);
+ }, 0);
+
+ // Update selection info
+ let selectionInfo = document.querySelector('.selection-info');
+ if (!selectionInfo) {
+ // Create selection info if it doesn't exist
+ selectionInfo = document.createElement('div');
+ selectionInfo.className = 'selection-info';
+ selectionInfo.style.cssText = 'margin: 0.5rem 0; font-size: 0.875rem; color: var(--text-muted);';
+
+ const controlsDiv = document.querySelector('.selection-controls');
+ if (controlsDiv) {
+ controlsDiv.parentNode.insertBefore(selectionInfo, controlsDiv.nextSibling);
+ }
+ }
+
+ // Format the selection information
+ if (count === 0) {
+ selectionInfo.textContent = 'No items selected';
+ } else {
+ const sizeStr = this.formatFileSize(totalSize);
+ const parts = [];
+
+ if (fileCount > 0) {
+ parts.push(`${fileCount} file${fileCount !== 1 ? 's' : ''}`);
+ }
+ if (folderCount > 0) {
+ parts.push(`${folderCount} folder${folderCount !== 1 ? 's' : ''}`);
+ }
+
+ let infoText = `${count} item${count !== 1 ? 's' : ''} selected`;
+ if (parts.length > 0) {
+ infoText += ` (${parts.join(', ')})`;
+ }
+ if (sizeStr) {
+ infoText += ` - Total size: ${sizeStr}`;
+ }
+
+ selectionInfo.textContent = infoText;
+ }
+
+ // Update download button text and state
+ const downloadBtn = document.getElementById('bulk-download-btn');
+ if (downloadBtn) {
+ console.log(`Updating download button: count=${count}, disabled=${count === 0}`);
+ if (count === 0) {
+ downloadBtn.innerHTML = ' Download Selected Items';
+ } else {
+ const sizeStr = this.formatFileSize(totalSize);
+ const btnText = sizeStr
+ ? `Download ${count} item${count !== 1 ? 's' : ''} (${sizeStr})`
+ : `Download ${count} item${count !== 1 ? 's' : ''}`;
+ downloadBtn.innerHTML = ` ${btnText}`;
+ }
+ // Use the validation function to determine button state
+ this.updateBulkDownloadButtonState();
+ } else {
+ console.error('Download button not found!');
+ }
+ }
+
+ formatFileSize(bytes) {
+ if (!bytes || bytes === 0) return '';
+
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ let size = bytes;
+ let unitIndex = 0;
+
+ while (size >= 1024 && unitIndex < units.length - 1) {
+ size /= 1024;
+ unitIndex++;
+ }
+
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ // Make tree items focusable for better accessibility
+ makeTreeItemsFocusable() {
+ const treeItems = document.querySelectorAll('.tree-item');
+ treeItems.forEach((item, index) => {
+ item.setAttribute('tabindex', index === 0 ? '0' : '-1');
+ item.addEventListener('focus', () => {
+ // Remove focus from other tree items
+ treeItems.forEach(otherItem => {
+ if (otherItem !== item) {
+ otherItem.setAttribute('tabindex', '-1');
+ }
+ });
+ item.setAttribute('tabindex', '0');
+ });
+ });
+ }
+
+ showTreeItemContextMenu(event, item, itemDiv) {
+ // Remove any existing context menu
+ const existingMenu = document.querySelector('.tree-context-menu');
+ if (existingMenu) {
+ existingMenu.remove();
+ }
+
+ // Create context menu
+ const contextMenu = document.createElement('div');
+ contextMenu.className = 'tree-context-menu';
+ contextMenu.style.cssText = `
+ position: fixed;
+ left: ${event.clientX}px;
+ top: ${event.clientY}px;
+ background: white;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ z-index: 1000;
+ min-width: 150px;
+ `;
+
+ const checkbox = itemDiv.querySelector('input[type="checkbox"]');
+ const isSelected = checkbox.checked;
+
+ const menuItems = [
+ {
+ label: isSelected ? 'Deselect Item' : 'Select Item',
+ action: () => {
+ checkbox.checked = !checkbox.checked;
+ checkbox.dispatchEvent(new Event('change'));
+ }
+ }
+ ];
+
+ // Add folder-specific options
+ const itemType = item.type || item.item_type || 'file';
+ if (itemType === 'folder') {
+ const toggleButton = itemDiv.querySelector('.tree-toggle');
+ const isExpanded = toggleButton && toggleButton.classList.contains('expanded');
+
+ menuItems.push({
+ label: isExpanded ? 'Collapse Folder' : 'Expand Folder',
+ action: () => {
+ if (toggleButton && !toggleButton.classList.contains('leaf')) {
+ toggleButton.click();
+ }
+ }
+ });
+ }
+
+ menuItems.push(
+ { separator: true },
+ {
+ label: 'Show Info',
+ action: () => {
+ this.showItemInfo(item);
+ }
+ }
+ );
+
+ // Build menu HTML
+ const menuHTML = menuItems.map(menuItem => {
+ if (menuItem.separator) {
+ return '';
+ }
+ return ``;
+ }).join('');
+
+ contextMenu.innerHTML = menuHTML;
+
+ // Add event listeners
+ contextMenu.addEventListener('click', (e) => {
+ const actionIndex = e.target.dataset.action;
+ if (actionIndex !== undefined) {
+ menuItems[actionIndex].action();
+ }
+ contextMenu.remove();
+ });
+
+ // Close menu when clicking outside
+ const closeMenu = (e) => {
+ if (!contextMenu.contains(e.target)) {
+ contextMenu.remove();
+ document.removeEventListener('click', closeMenu);
+ }
+ };
+
+ document.addEventListener('click', closeMenu);
+ document.body.appendChild(contextMenu);
+ }
+
+ showItemInfo(item) {
+ const itemType = item.type || item.item_type || 'file';
+ const infoText = `
+Item Information:
+
+Name: ${item.name}
+Type: ${itemType}
+Synapse ID: ${item.id}
+Size: ${item.size ? this.formatFileSize(item.size) : 'N/A'}
+Parent ID: ${item.parent_id || 'N/A'}
+ `.trim();
+
+ alert(infoText);
+ }
+
+ selectAllItems(select) {
+ const treeDiv = document.getElementById('items-tree');
+
+ if ($.fn.jstree && $(treeDiv).jstree) {
+ if (select) {
+ $(treeDiv).jstree('check_all');
+ } else {
+ $(treeDiv).jstree('uncheck_all');
+ }
+
+ // Manually update the selection after JSTree operations
+ // Use a short timeout to ensure JSTree has completed its operations
+ setTimeout(() => {
+ const checkedNodes = $(treeDiv).jstree('get_checked', true);
+ this.selectedItems.clear();
+
+ checkedNodes.forEach(node => {
+ if (node.data && node.data.item) {
+ this.selectedItems.add(node.data.item.id);
+ }
+ });
+
+ console.log(`Select all ${select ? 'enabled' : 'disabled'}: ${this.selectedItems.size} items selected`);
+ this.updateSelectionCount();
+ }, 10); // Slightly longer timeout to ensure completion
+ } else {
+ // Fallback to original method if JSTree is not available
+ const checkboxes = document.querySelectorAll('#items-tree input[type="checkbox"]');
+ this.selectedItems.clear();
+
+ checkboxes.forEach(checkbox => {
+ checkbox.checked = select;
+ const itemDiv = checkbox.closest('.tree-item');
+
+ if (select) {
+ this.selectedItems.add(checkbox.dataset.itemId);
+ itemDiv.classList.add('selected');
+ } else {
+ itemDiv.classList.remove('selected');
+ }
+ });
+
+ console.log(`Select all ${select ? 'enabled' : 'disabled'} (fallback): ${this.selectedItems.size} items selected`);
+ this.updateSelectionCount();
+ }
+ }
+
+ // Add convenience methods for expand/collapse all
+ expandAllFolders() {
+ const treeDiv = document.getElementById('items-tree');
+
+ if ($.fn.jstree && $(treeDiv).jstree) {
+ $(treeDiv).jstree('open_all');
+ } else {
+ // Fallback to original method
+ const toggleButtons = document.querySelectorAll('.tree-toggle.collapsed');
+ toggleButtons.forEach(button => {
+ button.click();
+ });
+ }
+ }
+
+ collapseAllFolders() {
+ const treeDiv = document.getElementById('items-tree');
+
+ if ($.fn.jstree && $(treeDiv).jstree) {
+ $(treeDiv).jstree('close_all');
+ } else {
+ // Fallback to original method
+ const toggleButtons = document.querySelectorAll('.tree-toggle.expanded');
+ toggleButtons.forEach(button => {
+ button.click();
+ });
+ }
+ }
+
+ async handleBulkDownload() {
+ const downloadPath = document.getElementById('bulk-download-location').value.trim();
+
+ if (this.selectedItems.size === 0) {
+ this.logMessage('Please select items to download', true);
+ return;
+ }
+
+ if (!downloadPath) {
+ this.logMessage('Download location is required', true);
+ return;
+ }
+
+ try {
+ const selectedItemsData = this.containerItems.filter(item =>
+ this.selectedItems.has(item.id)
+ );
+
+ const result = await window.electronAPI.bulkDownload({
+ items: selectedItemsData,
+ download_path: downloadPath,
+ create_subfolders: document.getElementById('recursive').checked
+ });
+
+ if (result.success) {
+ this.logMessage(`Bulk download initiated for ${selectedItemsData.length} items`, false);
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ this.logMessage(`Bulk download error: ${error.message}`, true);}
+ }
+
+ async handleScanUploadDirectory() {
+ const uploadPath = document.getElementById('bulk-upload-location').value.trim();
+
+ if (!uploadPath) {
+ this.logMessage('Please select a directory to scan', true);
+ return;
+ }
+
+ try {
+ this.logMessage('Scanning directory for files...', false);
+
+ const recursive = document.getElementById('bulk-preserve-structure').checked;
+
+ // Call backend API to scan directory
+ const result = await window.electronAPI.scanDirectory({
+ directory_path: uploadPath,
+ recursive: recursive
+ });
+
+ if (result.success) {
+ this.displayUploadFileContents(result.data.files);
+ const summary = result.data.summary;
+ this.logMessage(
+ `Found ${summary.file_count} files and ${summary.folder_count} folders ` +
+ `(${this.formatFileSize(summary.total_size)} total)`, false
+ );
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ this.logMessage(`Directory scan error: ${error.message}`, true);
+ }
+ }
+
+ displayUploadFileContents(files) {
+ this.uploadFileItems = files;
+ this.selectedUploadFiles.clear();
+
+ const contentsDiv = document.getElementById('bulk-upload-files');
+ const treeDiv = document.getElementById('upload-files-tree');
+
+ treeDiv.innerHTML = '';
+
+ // Build hierarchical structure for files
+ const { rootItems, parentMap } = this.buildUploadHierarchy(files);
+
+ // Create tree elements
+ this.createUploadTreeElements(rootItems, treeDiv, parentMap);
+
+ contentsDiv.style.display = 'block';
+ this.updateUploadSelectionDisplay();
+ }
+
+ buildUploadHierarchy(files) {
+ const itemsMap = new Map();
+ const parentMap = new Map();
+ const rootItems = [];
+
+ // Separate files and folders
+ const actualFiles = files.filter(item => item.type === 'file');
+ const actualFolders = files.filter(item => item.type === 'folder');
+
+ // First, add all actual folders to the maps
+ actualFolders.forEach(folder => {
+ const normalizedPath = folder.relative_path.replace(/\\/g, '/');
+ if (!itemsMap.has(normalizedPath)) {
+ const folderObj = {
+ ...folder,
+ id: `folder_${folder.path}`,
+ name: folder.name,
+ type: 'folder',
+ path: normalizedPath,
+ isVirtual: false
+ };
+ itemsMap.set(normalizedPath, folderObj);
+ }
+ });
+
+ // Create virtual folders for any missing parent directories of files
+ actualFiles.forEach(file => {
+ // Handle both Windows (\) and Unix (/) path separators
+ const pathParts = file.relative_path.replace(/\\/g, '/').split('/').filter(part => part.length > 0);
+
+ // Create all parent directories as virtual folders (only if they don't already exist as actual folders)
+ let currentPath = '';
+ for (let i = 0; i < pathParts.length - 1; i++) {
+ const part = pathParts[i];
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
+
+ if (!itemsMap.has(currentPath)) {
+ const virtualFolder = {
+ id: `folder_${currentPath}`,
+ name: part,
+ type: 'folder',
+ path: currentPath,
+ isVirtual: true
+ };
+ itemsMap.set(currentPath, virtualFolder);
+ }
+ }
+ });
+
+ // Now build the hierarchy relationships
+ itemsMap.forEach((item, path) => {
+ const pathParts = path.split('/');
+ if (pathParts.length === 1) {
+ // Root level item
+ rootItems.push(item);
+ } else {
+ // Find parent
+ const parentPath = pathParts.slice(0, -1).join('/');
+ if (!parentMap.has(parentPath)) {
+ parentMap.set(parentPath, []);
+ }
+ parentMap.get(parentPath).push(item);
+ }
+ });
+
+ // Add files to their parent directories
+ actualFiles.forEach(file => {
+ const pathParts = file.relative_path.replace(/\\/g, '/').split('/').filter(part => part.length > 0);
+ const fileName = pathParts[pathParts.length - 1];
+ const fileObj = {
+ ...file,
+ id: `file_${file.path}`,
+ name: fileName,
+ type: 'file'
+ };
+
+ const parentPath = pathParts.length > 1 ? pathParts.slice(0, -1).join('/') : '';
+
+ if (parentPath) {
+ if (!parentMap.has(parentPath)) {
+ parentMap.set(parentPath, []);
+ }
+ parentMap.get(parentPath).push(fileObj);
+ } else {
+ rootItems.push(fileObj);
+ }
+ });
+
+ return { rootItems, parentMap };
+ }
+
+ createUploadTreeElements(items, container, parentMap, level = 0) {
+ items.forEach(item => {
+ const itemElement = this.createUploadTreeItem(item, level);
+ container.appendChild(itemElement);
+
+ // Add children if this is a folder and it has children
+ if (item.type === 'folder') {
+ const children = parentMap.get(item.path) || [];
+ const childrenContainer = document.createElement('div');
+ childrenContainer.className = 'tree-children';
+ // Create a safe ID by replacing problematic characters
+ const safeId = item.id.replace(/[^a-zA-Z0-9_-]/g, '_');
+ childrenContainer.id = `upload-children-${safeId}`;
+ childrenContainer.style.display = 'block'; // Start expanded like download tree
+
+ if (children.length > 0) {
+ this.createUploadTreeElements(
+ children,
+ childrenContainer,
+ parentMap,
+ level + 1
+ );
+ } else {
+ // Handle empty folders - add a placeholder or just leave empty
+ // The folder will still be expandable but show as empty
+ const emptyMessage = document.createElement('div');
+ emptyMessage.className = 'tree-item empty-folder level-' + (level + 1);
+ emptyMessage.innerHTML = `
+
+ Empty folder
+
+ `;
+ childrenContainer.appendChild(emptyMessage);
+ }
+
+ // Update the toggle button class based on whether folder has children
+ const toggleButton = itemElement.querySelector('.tree-toggle');
+ if (toggleButton) {
+ if (children.length === 0) {
+ toggleButton.classList.remove('expanded');
+ toggleButton.classList.add('leaf');
+ }
+ }
+
+ container.appendChild(childrenContainer);
+ }
+ });
+ }
+
+ createUploadTreeItem(item, level) {
+ const itemDiv = document.createElement('div');
+ itemDiv.className = `tree-item level-${level} upload-tree-item ${item.type === 'folder' ? 'folder' : 'file'}`;
+ itemDiv.dataset.id = item.id;
+ itemDiv.dataset.type = item.type;
+ itemDiv.dataset.level = level;
+
+ let content = '';
+
+ if (item.type === 'folder') {
+ // For folders: "Folder Name (folder)" - we'll add count later if needed
+ content = `
+
+
+
+ ${this.escapeHtml(item.name)} (folder)
+
+ `;
+ } else {
+ // For files: "File Name (file, size)"
+ const sizeStr = this.formatFileSize(item.size);
+ content = `
+
+
+
+
+ ${this.escapeHtml(item.name)} (${sizeStr})
+
+ `;
+ }
+
+ itemDiv.innerHTML = content;
+
+ // Add event listeners instead of inline onclick handlers
+ if (item.type === 'folder') {
+ const toggleButton = itemDiv.querySelector('.tree-toggle');
+ if (toggleButton) {
+ toggleButton.addEventListener('click', () => {
+ this.toggleUploadFolder(item.id);
+ });
+ }
+ } else {
+ // Add event listener for file checkbox
+ itemDiv.addEventListener('change', (e) => {
+ if (e.target.classList.contains('item-checkbox')) {
+ this.handleUploadFileSelection(item.id, e.target.checked);
+ }
+ });
+ }
+
+ return itemDiv;
+ }
+
+ handleUploadFileSelection(fileId, selected) {
+ if (selected) {
+ this.selectedUploadFiles.add(fileId);
+ } else {
+ this.selectedUploadFiles.delete(fileId);
+ }
+ this.updateUploadSelectionDisplay();
+ }
+
+ updateUploadSelectionDisplay() {
+ const selectedCount = this.selectedUploadFiles.size;
+ const totalFiles = this.uploadFileItems.filter(item => item.type === 'file').length;
+
+ const button = document.getElementById('bulk-upload-btn');
+ if (button) {
+ button.textContent = `Upload Selected Files (${selectedCount})`;
+ // Use the validation function instead of just checking count
+ this.updateBulkUploadButtonState();
+ }
+ }
+
+ selectAllUploadFiles(select) {
+ const checkboxes = document.querySelectorAll('#upload-files-tree .item-checkbox');
+ checkboxes.forEach(checkbox => {
+ checkbox.checked = select;
+ const fileId = checkbox.dataset.id;
+ if (select) {
+ this.selectedUploadFiles.add(fileId);
+ } else {
+ this.selectedUploadFiles.delete(fileId);
+ }
+ });
+ this.updateUploadSelectionDisplay();
+ }
+
+ toggleUploadFolder(itemId) {
+ // Find the tree item and toggle button using the dataset id
+ const treeItem = document.querySelector(`#upload-files-tree .tree-item[data-id="${CSS.escape(itemId)}"]`);
+ if (!treeItem) {
+ console.warn('Could not find tree item for ID:', itemId);
+ return;
+ }
+
+ const toggleButton = treeItem.querySelector('.tree-toggle');
+ // Create the same safe ID used when creating the container
+ const safeId = itemId.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const childrenDiv = document.getElementById(`upload-children-${safeId}`);
+
+ if (toggleButton && childrenDiv) {
+ const isExpanded = toggleButton.classList.contains('expanded');
+
+ if (isExpanded) {
+ toggleButton.classList.remove('expanded');
+ toggleButton.classList.add('collapsed');
+ childrenDiv.style.display = 'none';
+ } else {
+ toggleButton.classList.remove('collapsed');
+ toggleButton.classList.add('expanded');
+ childrenDiv.style.display = 'block';
+ }
+ } else {
+ console.warn('Could not find toggle button or children container for ID:', itemId, 'Safe ID:', safeId);
+ }
+ }
+
+ expandAllUploadFolders() {
+ const toggles = document.querySelectorAll('#upload-files-tree .tree-toggle');
+ toggles.forEach(toggle => {
+ if (!toggle.classList.contains('leaf')) {
+ const treeItem = toggle.closest('.tree-item');
+ const itemId = treeItem.dataset.id;
+ const safeId = itemId.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const childrenContainer = document.getElementById(`upload-children-${safeId}`);
+ if (childrenContainer) {
+ childrenContainer.style.display = 'block';
+ toggle.classList.remove('collapsed');
+ toggle.classList.add('expanded');
+ }
+ }
+ });
+ }
+
+ collapseAllUploadFolders() {
+ const toggles = document.querySelectorAll('#upload-files-tree .tree-toggle');
+ toggles.forEach(toggle => {
+ if (!toggle.classList.contains('leaf')) {
+ const treeItem = toggle.closest('.tree-item');
+ const itemId = treeItem.dataset.id;
+ const safeId = itemId.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const childrenContainer = document.getElementById(`upload-children-${safeId}`);
+ if (childrenContainer) {
+ childrenContainer.style.display = 'none';
+ toggle.classList.remove('expanded');
+ toggle.classList.add('collapsed');
+ }
+ }
+ });
+ }
+
+ formatFileSize(bytes) {
+ if (!bytes) return '';
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
+ }
+
+ async handleBulkUpload() {
+ const parentId = document.getElementById('bulk-parent-id').value.trim();
+ const preserveStructure = document.getElementById('bulk-preserve-structure').checked;
+
+ if (!parentId) {
+ this.logMessage('Parent ID is required for bulk upload', true);
+ return;
+ }
+
+ if (this.selectedUploadFiles.size === 0) {
+ this.logMessage('Please select files to upload', true);
+ return;
+ }
+
+ try {
+ // Get selected file data - handle both old and new ID formats
+ const selectedFileData = this.uploadFileItems.filter(file => {
+ const fileId = `file_${file.path}`;
+ return this.selectedUploadFiles.has(fileId) || this.selectedUploadFiles.has(file.id);
+ });
+
+ const result = await window.electronAPI.bulkUpload({
+ parent_id: parentId,
+ files: selectedFileData,
+ preserve_folder_structure: preserveStructure
+ });
+
+ if (result.success) {
+ this.logMessage(`Bulk upload initiated for ${selectedFileData.length} files`, false);
+ } else {
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ this.logMessage(`Bulk upload error: ${error.message}`, true);}
+ }
+
+ setOperationInProgress(operation, message) {
+ const progressText = document.getElementById(`${operation}-progress-text`);
+ const progressBar = document.getElementById(`${operation}-progress`);
+ const button = document.getElementById(`${operation}-btn`);
+
+ if (progressText) progressText.textContent = message;
+ if (progressBar) {
+ progressBar.style.width = '0%';
+ progressBar.classList.remove('error'); // Remove any error styling
+ }
+ if (button) {
+ // Button state management removed
+ // Add visual indicator that operation is in progress
+ if (!button.innerHTML.includes('fa-spinner')) {
+ const originalText = button.innerHTML;
+ button.dataset.originalText = originalText;
+ button.innerHTML = ' Processing...';
+ }
+ }
+
+ this.setStatus(message);
+ }
+
+ updateProgress(operation, progress, message) {
+ const progressText = document.getElementById(`${operation}-progress-text`);
+ const progressBar = document.getElementById(`${operation}-progress`);
+
+ if (progressText) progressText.textContent = message;
+ if (progressBar) progressBar.style.width = `${progress}%`;
+
+ this.setStatus(`${operation}: ${progress}%`);
+ }
+
+ setOperationComplete(operation, success) {
+ const progressText = document.getElementById(`${operation}-progress-text`);
+ const progressBar = document.getElementById(`${operation}-progress`);
+ const button = document.getElementById(`${operation}-btn`);
+
+ if (progressText) {
+ progressText.textContent = success ? 'Operation completed' : 'Operation failed';
+ }
+ if (progressBar) {
+ progressBar.style.width = success ? '100%' : '0%';
+ if (success) {
+ progressBar.classList.remove('error');
+ } else {
+ progressBar.classList.add('error');
+ }
+ }
+ if (button) {
+ // Restore original button text if it was saved
+ if (button.dataset.originalText) {
+ button.innerHTML = button.dataset.originalText;
+ delete button.dataset.originalText;
+ }
+ // Don't just enable the button - check if it should be enabled based on current form state
+ }
+
+ this.setStatus(success ? 'Operation completed' : 'Operation failed');
+ }
+
+ updateButtonStateForOperation(operation) {
+ switch (operation) {
+ case 'upload':
+ this.updateUploadButtonState();
+ break;
+ case 'download':
+ this.updateDownloadButtonState();
+ break;
+ case 'bulk-upload':
+ this.updateBulkUploadButtonState();
+ break;
+ case 'bulk-download':
+ this.updateBulkDownloadButtonState();
+ break;
+ default:
+ // For other operations, just enable the button
+ const button = document.getElementById(`${operation}-btn`);
+ if (button) {
+ // Button state management removed - no action needed
+ }
+ break;
+ }
+ }
+
+ validateUploadForm() {
+ const filePathEl = document.getElementById('upload-file');
+ const modeEl = document.querySelector('input[name="uploadMode"]:checked');
+ const parentIdEl = document.getElementById('parent-id');
+ const entityIdEl = document.getElementById('entity-id');
+
+ if (!filePathEl || !parentIdEl || !entityIdEl) return false;
+
+ const filePath = filePathEl.value.trim();
+ const mode = modeEl?.value;
+ const parentId = parentIdEl.value.trim();
+ const entityId = entityIdEl.value.trim();
+
+ if (!filePath) return false;
+ if (mode === 'new' && !parentId) return false;
+ if (mode === 'update' && !entityId) return false;
+
+ return true;
+ }
+
+ validateDownloadForm() {
+ const synapseIdEl = document.getElementById('download-id');
+ const downloadPathEl = document.getElementById('download-location');
+
+ if (!synapseIdEl || !downloadPathEl) return false;
+
+ const synapseId = synapseIdEl.value.trim();
+ const downloadPath = downloadPathEl.value.trim();
+
+ return synapseId && downloadPath;
+ }
+
+ updateUploadButtonState() {
+ const button = document.getElementById('upload-btn');
+ if (button) {
+ // Only update state if not currently processing (no spinner)
+ const isProcessing = button.innerHTML.includes('fa-spinner') || button.disabled && button.innerHTML.includes('Processing');
+ if (!isProcessing) {
+ // Button state management removed
+ }
+ }
+ }
+
+ updateDownloadButtonState() {
+ const button = document.getElementById('download-btn');
+ if (button) {
+ // Only update state if not currently processing (no spinner)
+ const isProcessing = button.innerHTML.includes('fa-spinner') || button.disabled && button.innerHTML.includes('Processing');
+ if (!isProcessing) {
+ // Button state management removed
+ }
+ }
+ }
+
+ updateBulkUploadButtonState() {
+ const button = document.getElementById('bulk-upload-btn');
+ const parentIdEl = document.getElementById('bulk-parent-id');
+
+ if (button && parentIdEl) {
+ // Only update state if not currently processing
+ const isProcessing = button.innerHTML.includes('fa-spinner') || button.disabled && button.innerHTML.includes('Processing');
+ if (!isProcessing) {
+ const parentId = parentIdEl.value.trim();
+ const hasSelectedFiles = this.selectedUploadFiles && this.selectedUploadFiles.size > 0;
+ // Button state management removed
+ }
+ }
+ }
+
+ updateBulkDownloadButtonState() {
+ const button = document.getElementById('bulk-download-btn');
+ const downloadPathEl = document.getElementById('bulk-download-location');
+
+ if (button && downloadPathEl) {
+ // Only update state if not currently processing
+ const isProcessing = button.innerHTML.includes('fa-spinner') || button.disabled && button.innerHTML.includes('Processing');
+ if (!isProcessing) {
+ const downloadPath = downloadPathEl.value.trim();
+ const hasSelectedItems = this.selectedItems && this.selectedItems.size > 0;
+ // Button state management removed
+ }
+ }
+ }
+
+ handleOperationComplete(operation, success, data) {
+ console.log(`Operation complete: ${operation}, success: ${success}`, data);
+
+ if (success) {
+ this.logMessage(`${operation} completed successfully`, false);
+ if (data && data.message) {
+ this.logMessage(data.message, false);
+ }
+ } else {
+ this.logMessage(`${operation} failed`, true);
+ if (data && data.error) {
+ this.logMessage(data.error, true);
+ }
+ }
+ }
+
+ logMessage(message, isError = false) {
+ const outputLog = document.getElementById('output-log');
+ const timestamp = new Date().toLocaleTimeString();
+ const logClass = isError ? 'log-error' : 'log-success';
+ const icon = isError ? '❌' : '✅';
+
+ const logEntry = document.createElement('div');
+ logEntry.className = `log-entry ${logClass}`;
+ logEntry.textContent = `[${timestamp}] ${icon} ${message}`;
+
+ outputLog.appendChild(logEntry);
+ this.updateLogCount();
+ this.autoScrollIfEnabled();
+ }
+
+ logMessageAdvanced(logMessage) {
+ const outputLog = document.getElementById('output-log');
+ const timestamp = logMessage.timestamp ?
+ new Date(logMessage.timestamp * 1000).toLocaleTimeString() :
+ new Date().toLocaleTimeString();
+
+ // Determine icon based on level
+ const icons = {
+ 'error': '❌',
+ 'critical': '🚨',
+ 'warning': '⚠️',
+ 'warn': '⚠️',
+ 'info': 'ℹ️',
+ 'debug': '🔍',
+ 'success': '✅'
+ };
+
+ const icon = icons[logMessage.level] || 'ℹ️';
+ const logClass = `log-${logMessage.level || 'info'}`;
+
+ const logEntry = document.createElement('div');
+ logEntry.className = `log-entry ${logClass}`;
+
+ // Create structured log entry
+ let logContent = '';
+ logContent += `[${timestamp}]`;
+
+ if (logMessage.operation) {
+ logContent += `[${logMessage.operation}]`;
+ }
+
+ if (logMessage.source && logMessage.source !== 'synapse-backend') {
+ logContent += `[${logMessage.source}]`;
+ }
+
+ logContent += ` ${icon} ${this.escapeHtml(logMessage.message)}`;
+
+ logEntry.innerHTML = logContent;
+ outputLog.appendChild(logEntry);
+
+ this.updateLogCount();
+
+ // Always attempt auto-scroll for real-time updates
+ this.autoScrollIfEnabled();
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ updateLogCount() {
+ const outputLog = document.getElementById('output-log');
+ const count = outputLog.children.length;
+ const logCountElement = document.getElementById('log-count');
+ if (logCountElement) {
+ logCountElement.textContent = `${count} message${count !== 1 ? 's' : ''}`;
+ }
+ }
+
+ autoScrollIfEnabled() {
+ if (!this.isAutoScrollEnabled()) {
+ return;
+ }
+
+ const now = Date.now();
+
+ // Throttle scroll operations to prevent excessive scrolling
+ if (now - this.lastScrollTime < this.scrollThrottleDelay) {
+ if (!this.pendingScroll) {
+ this.pendingScroll = true;
+ setTimeout(() => {
+ this.pendingScroll = false;
+ if (this.isAutoScrollEnabled()) {
+ this.performScroll();
+ }
+ }, this.scrollThrottleDelay);
+ }
+ return;
+ }
+
+ this.performScroll();
+ }
+
+ performScroll() {
+ this.lastScrollTime = Date.now();
+
+ // Use requestAnimationFrame to ensure DOM has updated before scrolling
+ requestAnimationFrame(() => {
+ this.scrollToBottom();
+ });
+ }
+
+ isAutoScrollEnabled() {
+ return this.autoScrollEnabled !== false; // Default to true
+ }
+
+ scrollToBottom() {
+ const outputLog = document.getElementById('output-log');
+ if (!outputLog) return;
+
+ // For real-time scrolling, we want immediate results, so use instant scroll
+ // but also ensure it gets to the very bottom
+ const targetScrollTop = outputLog.scrollHeight - outputLog.clientHeight;
+
+ // Set scroll position immediately
+ outputLog.scrollTop = targetScrollTop;
+
+ // Double-check after a short delay to handle any DOM updates
+ setTimeout(() => {
+ const newTargetScrollTop = outputLog.scrollHeight - outputLog.clientHeight;
+ if (outputLog.scrollTop < newTargetScrollTop - 5) { // 5px tolerance
+ outputLog.scrollTop = newTargetScrollTop;
+ }
+ }, 10);
+ }
+
+ toggleAutoScroll() {
+ this.autoScrollEnabled = !this.isAutoScrollEnabled();
+ this.updateAutoScrollUI();
+ }
+
+ updateAutoScrollUI() {
+ const toggleBtn = document.getElementById('auto-scroll-toggle');
+ const statusSpan = document.getElementById('auto-scroll-status');
+
+ if (this.isAutoScrollEnabled()) {
+ toggleBtn.classList.add('auto-scroll-enabled');
+ toggleBtn.classList.remove('auto-scroll-disabled');
+ statusSpan.textContent = 'Auto-scroll: ON';
+ statusSpan.className = 'auto-scroll-enabled';
+ } else {
+ toggleBtn.classList.remove('auto-scroll-enabled');
+ toggleBtn.classList.add('auto-scroll-disabled');
+ statusSpan.textContent = 'Auto-scroll: OFF';
+ statusSpan.className = 'auto-scroll-disabled';
+ }
+ }
+
+ handleScrollCommand(message) {
+ switch (message.action) {
+ case 'scroll_to_bottom':
+ this.scrollToBottom();
+ break;
+ default:
+ console.log('Unknown scroll command:', message.action);
+ }
+ }
+
+ clearOutput() {
+ document.getElementById('output-log').innerHTML = '';
+ this.updateLogCount();
+
+ // Reset scroll state
+ this.pendingScroll = false;
+ this.lastScrollTime = 0;
+ }
+
+ handleManualScroll(event) {
+ // Manual scrolling no longer affects auto-scroll state
+ // Auto-scroll will continue to work regardless of manual scrolling
+ // Only the explicit toggle button can disable auto-scroll
+ }
+
+ setStatus(message) {
+ document.getElementById('status-message').textContent = message;
+ }
+
+ showStatus(elementId, message, type) {
+ const element = document.getElementById(elementId);
+ element.textContent = message;
+ element.className = `status-message ${type}`;
+ element.style.display = 'block';
+
+ // Auto-hide after 5 seconds
+ setTimeout(() => {
+ element.style.display = 'none';
+ }, 5000);
+ }
+}
+
+// Initialize the application when the DOM is loaded
+document.addEventListener('DOMContentLoaded', () => {
+ window.synapseApp = new SynapseDesktopClient();
+});
diff --git a/synapse-electron/src/index.html b/synapse-electron/src/index.html
new file mode 100644
index 000000000..34ab19415
--- /dev/null
+++ b/synapse-electron/src/index.html
@@ -0,0 +1,400 @@
+
+
+
+
+
+ Synapse Desktop Client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Login to Synapse
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 messages
+ Auto-scroll: ON
+
+
+
+
+
+
+
Ready
+
+
+
+ Connected
+
+
+
+
+
+
+
+
+
+
diff --git a/synapse-electron/src/styles.css b/synapse-electron/src/styles.css
new file mode 100644
index 000000000..216ef0530
--- /dev/null
+++ b/synapse-electron/src/styles.css
@@ -0,0 +1,1309 @@
+/* Modern Synapse Desktop Client Styles */
+:root {
+ --primary-color: #1f4e79;
+ --primary-hover: #164066;
+ --secondary-color: #6c757d;
+ --success-color: #28a745;
+ --danger-color: #dc3545;
+ --warning-color: #ffc107;
+ --info-color: #17a2b8;
+ --light-color: #f8f9fa;
+ --dark-color: #343a40;
+ --border-color: #dee2e6;
+ --background-color: #ffffff;
+ --text-color: #212529;
+ --text-muted: #6c757d;
+ --shadow: 0 2px 4px rgba(0,0,0,0.1);
+ --shadow-lg: 0 4px 6px rgba(0,0,0,0.1);
+ --border-radius: 8px;
+ --border-radius-sm: 4px;
+ --transition: all 0.3s ease;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-color);
+ background-color: var(--light-color);
+ overflow: hidden;
+}
+
+#app {
+ width: 100%;
+ height: 100vh;
+ display: block;
+}
+
+.app-header {
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
+ color: white;
+ padding: 1rem;
+ box-shadow: var(--shadow);
+ position: relative;
+ z-index: 1000;
+}
+
+.header-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.header-icon {
+ font-size: 2rem;
+ margin-right: 1rem;
+}
+
+.app-header h1 {
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+.app-version {
+ position: absolute;
+ right: 0;
+ font-size: 0.875rem;
+ opacity: 0.8;
+}
+
+.main-container {
+ height: calc(100vh - 80px);
+ display: flex;
+ flex-direction: column;
+}
+
+/* Login Styles */
+.login-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ padding: 2rem;
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
+}
+
+.login-card {
+ background: var(--background-color);
+ padding: 2rem;
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-lg);
+ min-width: 400px;
+ max-width: 500px;
+}
+
+.login-card h2 {
+ color: var(--primary-color);
+ margin-bottom: 1.5rem;
+ text-align: center;
+ font-weight: 600;
+}
+
+.login-card h2 i {
+ margin-right: 0.5rem;
+}
+
+.login-modes {
+ margin-bottom: 1.5rem;
+}
+
+.radio-option {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.75rem;
+ cursor: pointer;
+ user-select: none;
+}
+
+.radio-option input[type="radio"] {
+ display: none;
+}
+
+.radio-custom {
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--border-color);
+ border-radius: 50%;
+ margin-right: 0.75rem;
+ position: relative;
+ transition: var(--transition);
+}
+
+.radio-option input[type="radio"]:checked + .radio-custom {
+ border-color: var(--primary-color);
+}
+
+.radio-option input[type="radio"]:checked + .radio-custom::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 10px;
+ height: 10px;
+ background-color: var(--primary-color);
+ border-radius: 50%;
+}
+
+.login-form {
+ display: none;
+}
+
+.login-form.active {
+ display: block;
+}
+
+.form-group {
+ margin-bottom: 1rem;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ color: var(--dark-color);
+}
+
+.form-group label.checkbox-label {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.75rem;
+ margin-top: 0.25rem;
+ font-weight: 400;
+ cursor: pointer;
+}
+
+.form-group label i {
+ margin-right: 0.5rem;
+ color: var(--primary-color);
+}
+
+.form-group input,
+.form-group select {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ font-size: 14px;
+ transition: var(--transition);
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(31, 78, 121, 0.1);
+}
+
+.profile-info {
+ font-size: 0.875rem;
+ color: var(--info-color);
+ margin-top: 0.5rem;
+}
+
+/* Button Styles */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: var(--border-radius-sm);
+ font-size: 14px;
+ font-weight: 500;
+ text-decoration: none;
+ cursor: pointer;
+ transition: var(--transition);
+ min-height: 44px;
+}
+
+.btn i {
+ margin-right: 0.5rem;
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background-color: var(--primary-hover);
+}
+
+.btn-secondary {
+ background-color: var(--secondary-color);
+ color: white;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background-color: #5a6268;
+}
+
+.btn-small {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ min-height: 36px;
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+#login-btn {
+ width: 100%;
+ margin-top: 1rem;
+}
+
+/* Status Messages */
+.status-message {
+ text-align: center;
+ margin-top: 1rem;
+ padding: 0.75rem;
+ border-radius: var(--border-radius-sm);
+ display: none;
+}
+
+.status-message.success {
+ background-color: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+ display: block;
+}
+
+.status-message.error {
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+ display: block;
+}
+
+.status-message.info {
+ background-color: #d1ecf1;
+ color: #0c5460;
+ border: 1px solid #bee5eb;
+ display: block;
+}
+
+/* Main Application */
+.main-app {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.user-info-bar {
+ background-color: var(--background-color);
+ border-bottom: 1px solid var(--border-color);
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.user-details {
+ display: flex;
+ align-items: center;
+ color: var(--dark-color);
+ font-weight: 500;
+}
+
+.user-details i {
+ margin-right: 0.5rem;
+ color: var(--primary-color);
+ font-size: 1.2rem;
+}
+
+/* Tab Navigation */
+.tab-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ height: calc(100% - 60px);
+}
+
+.tab-navigation {
+ background-color: var(--background-color);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ padding: 0 1rem;
+}
+
+.tab-btn {
+ padding: 1rem 1.5rem;
+ border: none;
+ background: none;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-muted);
+ border-bottom: 3px solid transparent;
+ transition: var(--transition);
+}
+
+.tab-btn:hover {
+ color: var(--primary-color);
+ background-color: var(--light-color);
+}
+
+.tab-btn.active {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+}
+
+.tab-btn i {
+ margin-right: 0.5rem;
+}
+
+.tab-content {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.tab-panel {
+ display: none;
+ height: 100%;
+ padding: 1.5rem;
+}
+
+.tab-panel.active {
+ display: block;
+}
+
+/* Form Sections */
+.form-section {
+ background-color: var(--background-color);
+ border-radius: var(--border-radius);
+ padding: 1.5rem;
+ box-shadow: var(--shadow);
+ margin-bottom: 1rem;
+}
+
+.form-section h3 {
+ color: var(--primary-color);
+ margin-bottom: 1.5rem;
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+.form-section h3 i {
+ margin-right: 0.5rem;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.form-group {
+ margin-bottom: 1rem;
+}
+
+.file-input-group {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.file-input-group input {
+ flex: 1;
+}
+
+/* Upload Mode Section */
+.upload-mode-section {
+ margin: 1.5rem 0;
+ padding: 1rem;
+ background-color: var(--light-color);
+ border-radius: var(--border-radius-sm);
+}
+
+.upload-mode-section h4 {
+ margin-bottom: 1rem;
+ color: var(--dark-color);
+}
+
+.radio-group {
+ display: flex;
+ gap: 1.5rem;
+}
+
+/* Checkbox Styles */
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+ line-height: 1.4;
+ min-height: 24px;
+}
+
+.checkbox-label input[type="checkbox"] {
+ display: none;
+}
+
+.checkbox-custom {
+ width: 18px;
+ height: 18px;
+ border: 2px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ margin-right: 0.6rem;
+ position: relative;
+ transition: var(--transition);
+ flex-shrink: 0;
+ background-color: var(--background-color);
+}
+
+.checkbox-custom:hover {
+ border-color: var(--primary-color);
+}
+
+.checkbox-label:focus-within .checkbox-custom {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(31, 78, 121, 0.1);
+}
+
+.checkbox-label input[type="checkbox"]:checked + .checkbox-custom {
+ border-color: var(--primary-color);
+ background-color: var(--primary-color);
+}
+
+.checkbox-label input[type="checkbox"]:checked + .checkbox-custom::after {
+ content: '\f00c';
+ font-family: 'Font Awesome 6 Free';
+ font-weight: 900;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: white;
+ font-size: 10px;
+ line-height: 1;
+}
+
+/* Standard HTML checkboxes for tree items */
+input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ margin: 0;
+ padding: 0;
+}
+
+input[type="checkbox"]:focus {
+ outline: 2px solid var(--primary-color);
+ outline-offset: 2px;
+}
+
+/* Progress Styles */
+.progress-section {
+ margin-top: 1.5rem;
+}
+
+.progress-text {
+ margin-bottom: 0.5rem;
+ font-size: 0.875rem;
+ color: var(--text-muted);
+ min-height: 1.25rem;
+}
+
+.progress-bar {
+ height: 8px;
+ background-color: var(--border-color);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background-color: var(--primary-color);
+ width: 0%;
+ transition: width 0.3s ease;
+}
+
+/* Container Contents */
+.container-contents {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ background-color: var(--light-color);
+ border-radius: var(--border-radius-sm);
+}
+
+.container-contents h4 {
+ margin-bottom: 1rem;
+ color: var(--dark-color);
+}
+
+.selection-controls {
+ margin-bottom: 1rem;
+ display: flex;
+ gap: 0.5rem;
+}
+
+.items-tree {
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--background-color);
+ padding: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+/* Tree View Styles */
+.items-tree {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--background-color);
+ margin-bottom: 1rem;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+/* JSTree customizations */
+.items-tree .jstree-default .jstree-node {
+ min-height: 28px;
+ line-height: 28px;
+ margin-left: 0;
+}
+
+.items-tree .jstree-default .jstree-anchor {
+ padding: 2px 6px;
+ font-size: 14px;
+ color: var(--text-color);
+}
+
+.items-tree .jstree-default .jstree-anchor:hover {
+ background-color: var(--light-color);
+ color: var(--text-color);
+}
+
+.items-tree .jstree-default .jstree-clicked {
+ background-color: rgba(31, 78, 121, 0.1);
+ color: var(--text-color);
+}
+
+.items-tree .jstree-default .jstree-checkbox {
+ margin-right: 8px;
+ width: 16px !important;
+ height: 16px !important;
+ background-size: 16px 16px !important;
+}
+
+/* Folder and file icons */
+.items-tree .jstree-default .jstree-folder .jstree-icon {
+ color: #4a90e2;
+}
+
+.items-tree .jstree-default .jstree-file .jstree-icon {
+ color: #666;
+}
+
+/* Override default JSTree theme colors */
+.items-tree .jstree-default .jstree-node {
+ background: transparent;
+}
+
+.items-tree .jstree-default .jstree-anchor.jstree-hovered {
+ background-color: var(--light-color);
+ box-shadow: none;
+}
+
+/* Fallback styles for custom tree implementation */
+.items-tree .tree-item {
+ padding: 0.5rem;
+ border-bottom: 1px solid var(--border-color);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ transition: var(--transition);
+ user-select: none;
+ position: relative;
+ min-height: 40px; /* Ensure adequate height */
+ height: auto;
+ width: 100%;
+ overflow: visible; /* Allow content to be visible */
+ box-sizing: border-box;
+ flex-wrap: nowrap; /* Prevent wrapping to next line */
+}
+
+.items-tree .tree-item:hover {
+ background-color: var(--light-color);
+}
+
+.items-tree .tree-item.selected {
+ background-color: rgba(31, 78, 121, 0.1);
+}
+
+/* Hierarchical indentation */
+.items-tree .tree-item.level-0 { padding-left: 0.5rem; }
+.items-tree .tree-item.level-1 { padding-left: 1.5rem; }
+.items-tree .tree-item.level-2 { padding-left: 2.5rem; }
+.items-tree .tree-item.level-3 { padding-left: 3.5rem; }
+.items-tree .tree-item.level-4 { padding-left: 4.5rem; }
+.items-tree .tree-item.level-5 { padding-left: 5.5rem; }
+.items-tree .tree-item.level-6 { padding-left: 6.5rem; }
+
+/* Tree expand/collapse toggle */
+.items-tree .tree-toggle {
+ width: 16px;
+ height: 16px;
+ margin-right: 0.25rem;
+ cursor: pointer;
+ border: none;
+ background: none;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-muted);
+ transition: var(--transition);
+ position: relative;
+}
+
+/* Override JSTree styles that might interfere */
+.items-tree .tree-toggle::after {
+ display: none !important;
+}
+
+.items-tree .tree-toggle:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ border-radius: 2px;
+}
+
+.items-tree .tree-toggle.expanded::before {
+ content: '▼';
+}
+
+.items-tree .tree-toggle.collapsed::before {
+ content: '▶';
+}
+
+.items-tree .tree-toggle.leaf {
+ visibility: hidden;
+}
+
+.items-tree .tree-toggle.leaf::before {
+ display: none;
+}
+
+.items-tree .tree-item input[type="checkbox"] {
+ margin-right: 0.6rem;
+ margin-left: 0.25rem;
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ cursor: pointer;
+}
+
+.items-tree .tree-item-icon {
+ margin-right: 0.5rem;
+ color: var(--text-muted);
+ width: 16px;
+ text-align: center;
+}
+
+.items-tree .tree-item-icon.folder {
+ color: #4a90e2;
+}
+
+.items-tree .tree-item-icon.file {
+ color: #666;
+}
+
+.items-tree .tree-item-content {
+ display: table;
+ width: 100%;
+ table-layout: fixed;
+ min-width: 0;
+}
+
+.items-tree .tree-item-content > * {
+ display: table-cell;
+ vertical-align: middle;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0 0.25rem;
+}
+
+.items-tree .tree-item input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ margin-right: 0.6rem;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.items-tree .tree-item-icon {
+ width: 30px;
+ color: var(--text-muted);
+ text-align: center;
+}
+
+.items-tree .tree-item-icon.folder {
+ color: #4a90e2;
+}
+
+.items-tree .tree-item-icon.file {
+ color: #666;
+}
+
+.items-tree .tree-item-name {
+ width: 40%;
+}
+
+.items-tree .tree-item-type {
+ width: 15%;
+ text-align: left;
+}
+
+.items-tree .tree-item-size {
+ width: 15%;
+ text-align: right;
+}
+
+.items-tree .tree-item-path {
+ width: 30%;
+ font-style: italic;
+ text-align: left;
+ font-size: 0.875rem;
+ color: var(--text-muted);
+}
+
+/* Upload Tree Compact Styling */
+.items-tree .upload-tree-item {
+ padding: 0.25rem 0.5rem;
+}
+
+.items-tree .upload-tree-item .tree-item-content.upload-compact {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ table-layout: auto;
+ gap: 0.5rem;
+ margin: 0;
+ padding: 0;
+}
+
+.items-tree .upload-tree-item .tree-item-content.upload-compact > * {
+ display: initial;
+ vertical-align: middle;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0;
+ margin: 0;
+}
+
+.items-tree .upload-tree-item .tree-item-content.upload-compact .item-checkbox {
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+ margin-right: 0.5rem;
+ cursor: pointer;
+}
+
+.items-tree .upload-tree-item .tree-item-content.upload-compact .tree-item-icon {
+ flex-shrink: 0;
+ width: auto;
+ margin-right: 0.25rem;
+ font-size: 0.875rem;
+}
+
+.items-tree .upload-tree-item .tree-item-content.upload-compact .upload-item-info {
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 0.875rem;
+}
+
+.items-tree .upload-tree-item .tree-item-content.upload-compact .item-meta {
+ color: var(--text-muted);
+ font-style: italic;
+ font-size: 0.8rem;
+ font-weight: normal;
+}
+
+/* Override level padding for upload tree to be more compact */
+.items-tree .upload-tree-item.level-0 { padding-left: 0.5rem; }
+.items-tree .upload-tree-item.level-1 { padding-left: 1.25rem; }
+.items-tree .upload-tree-item.level-2 { padding-left: 2rem; }
+.items-tree .upload-tree-item.level-3 { padding-left: 2.75rem; }
+.items-tree .upload-tree-item.level-4 { padding-left: 3.5rem; }
+.items-tree .upload-tree-item.level-5 { padding-left: 4.25rem; }
+.items-tree .upload-tree-item.level-6 { padding-left: 5rem; }
+
+/* Additional JSTree interference prevention */
+.items-tree .tree-item::before,
+.items-tree .tree-item::after {
+ display: none !important;
+}
+
+.items-tree .tree-item:not(.tree-toggle)::before,
+.items-tree .tree-item:not(.tree-toggle)::after {
+ display: none !important;
+}
+
+/* Ensure only our toggle arrows show */
+.items-tree .tree-item > *:not(.tree-toggle)::before,
+.items-tree .tree-item > *:not(.tree-toggle)::after {
+ display: none !important;
+}
+
+/* Hidden children */
+.tree-item.collapsed + .tree-children {
+ display: none;
+}
+
+.tree-children {
+ display: block;
+}
+
+/* Selection Controls */
+.selection-controls {
+ margin-bottom: 1rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ background-color: var(--light-color);
+ border-radius: var(--border-radius-sm);
+ border: 1px solid var(--border-color);
+}
+
+.selection-controls .btn {
+ font-size: 0.875rem;
+ padding: 0.375rem 0.75rem;
+}
+
+.selection-info {
+ margin: 0.5rem 0;
+ font-size: 0.875rem;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.help-text {
+ background-color: rgba(31, 78, 121, 0.1);
+ border: 1px solid rgba(31, 78, 121, 0.2);
+ border-radius: var(--border-radius-sm);
+ padding: 0.75rem;
+ margin-bottom: 1rem;
+ font-size: 0.875rem;
+ color: var(--text-color);
+}
+
+.help-text i {
+ color: var(--primary-color);
+ margin-right: 0.5rem;
+}
+
+/* Context Menu */
+.tree-context-menu {
+ background: white;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ font-size: 0.875rem;
+ overflow: hidden;
+}
+
+.context-menu-item {
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+ transition: var(--transition);
+}
+
+.context-menu-item:hover {
+ background-color: var(--light-color);
+}
+
+.context-menu-separator {
+ height: 1px;
+ background-color: var(--border-color);
+ margin: 0.25rem 0;
+}
+
+.files-list {
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--background-color);
+ margin-bottom: 1rem;
+}
+
+.file-item {
+ padding: 0.75rem;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+}
+
+.file-item:last-child {
+ border-bottom: none;
+}
+
+.file-item-icon {
+ margin-right: 0.75rem;
+ color: var(--text-muted);
+}
+
+.file-item-info {
+ flex: 1;
+}
+
+.file-item-name {
+ font-weight: 500;
+ margin-bottom: 0.25rem;
+}
+
+.file-item-path {
+ font-size: 0.875rem;
+ color: var(--text-muted);
+}
+
+/* Output Section */
+.output-section {
+ background-color: var(--background-color);
+ border-top: 1px solid var(--border-color);
+ height: 300px; /* Increased height */
+ display: flex;
+ flex-direction: column;
+ min-height: 250px; /* Ensure minimum height */
+ position: sticky; /* Keep in view when possible */
+ bottom: 0;
+ z-index: 10; /* Ensure it stays on top */
+}
+
+/* Ensure the main container can scroll to accommodate the output section */
+.main-container {
+ height: calc(100vh - 80px); /* Account for header */
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Make sure the content area is flexible */
+.app-content {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.output-header {
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-shrink: 0; /* Don't shrink */
+ background-color: var(--background-color);
+}
+
+.output-header h3 {
+ color: var(--dark-color);
+ font-size: 1rem;
+ font-weight: 600;
+ margin: 0;
+}
+
+.output-header h3 i {
+ margin-right: 0.5rem;
+}
+
+.output-controls {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ flex-wrap: wrap; /* Allow wrapping on small screens */
+}
+
+.output-controls .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ white-space: nowrap;
+}
+
+.auto-scroll-enabled {
+ background-color: var(--success-color);
+ color: white;
+}
+
+.auto-scroll-disabled {
+ background-color: var(--secondary-color);
+ color: var(--text-color);
+}
+
+.output-log {
+ flex: 1;
+ padding: 1rem;
+ background-color: #1e1e1e;
+ color: #d4d4d4;
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+ font-size: 0.875rem;
+ line-height: 1.4;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ scroll-behavior: smooth; /* Smooth scrolling */
+ word-wrap: break-word; /* Prevent long lines from causing horizontal scroll */
+}
+
+.output-log .log-entry {
+ margin-bottom: 0.25rem;
+ padding: 0.125rem 0;
+ border-left: 3px solid transparent;
+ padding-left: 0.5rem;
+ word-wrap: break-word;
+}
+
+.output-log .log-error {
+ color: #f14c4c;
+ border-left-color: #f14c4c;
+ background-color: rgba(241, 76, 76, 0.1);
+}
+
+.output-log .log-success {
+ color: #4fc3f7;
+ border-left-color: #4fc3f7;
+}
+
+.output-log .log-warning {
+ color: #ffb74d;
+ border-left-color: #ffb74d;
+ background-color: rgba(255, 183, 77, 0.1);
+}
+
+.output-log .log-info {
+ color: #d4d4d4;
+ border-left-color: #4fc3f7;
+}
+
+.output-log .log-debug {
+ color: #9e9e9e;
+ border-left-color: #9e9e9e;
+ font-size: 0.8rem;
+}
+
+.output-log .log-critical {
+ color: #ff5252;
+ border-left-color: #ff5252;
+ background-color: rgba(255, 82, 82, 0.2);
+ font-weight: bold;
+}
+
+.output-log .log-timestamp {
+ color: #666;
+ font-size: 0.75rem;
+ margin-right: 0.5rem;
+}
+
+.output-log .log-source {
+ color: #888;
+ font-size: 0.75rem;
+ margin-right: 0.5rem;
+ font-style: italic;
+}
+
+.output-log .log-operation {
+ color: #4fc3f7;
+ font-size: 0.75rem;
+ margin-right: 0.5rem;
+ font-weight: bold;
+}
+
+.output-status {
+ padding: 0.5rem 1rem;
+ border-top: 1px solid var(--border-color);
+ background-color: rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ flex-shrink: 0; /* Don't shrink */
+}
+
+.output-status .auto-scroll-enabled {
+ color: var(--success-color);
+}
+
+.output-status .auto-scroll-disabled {
+ color: var(--secondary-color);
+}
+
+/* Responsive adjustments */
+@media (max-height: 600px) {
+ .output-section {
+ height: 200px;
+ min-height: 150px;
+ }
+}
+
+@media (max-width: 768px) {
+ .output-controls {
+ gap: 0.25rem;
+ }
+
+ .output-controls .btn {
+ padding: 0.2rem 0.4rem;
+ font-size: 0.7rem;
+ }
+
+ .output-header {
+ padding: 0.75rem;
+ }
+}
+
+/* Status Bar */
+.status-bar {
+ background-color: var(--background-color);
+ border-top: 1px solid var(--border-color);
+ padding: 0.5rem 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.875rem;
+}
+
+.status-right {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.connection-status {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.status-connected {
+ color: var(--success-color);
+}
+
+.status-disconnected {
+ color: var(--danger-color);
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+
+ .radio-group {
+ flex-direction: column;
+ gap: 0.75rem;
+ }
+
+ .tab-navigation {
+ flex-wrap: wrap;
+ }
+
+ .tab-btn {
+ padding: 0.75rem 1rem;
+ font-size: 0.875rem;
+ }
+}
+
+/* Loading States */
+.loading {
+ position: relative;
+ overflow: hidden;
+}
+
+.loading::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(31, 78, 121, 0.1),
+ transparent
+ );
+ animation: loading 1.5s infinite;
+}
+
+@keyframes loading {
+ 0% {
+ left: -100%;
+ }
+ 100% {
+ left: 100%;
+ }
+}
+
+/* Utility Classes */
+.hidden {
+ display: none !important;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-muted {
+ color: var(--text-muted);
+}
+
+.text-error {
+ color: var(--danger-color);
+}
+
+.text-success {
+ color: var(--success-color);
+}
+
+.mb-0 { margin-bottom: 0; }
+.mb-1 { margin-bottom: 0.5rem; }
+.mb-2 { margin-bottom: 1rem; }
+.mb-3 { margin-bottom: 1.5rem; }
+
+.mt-0 { margin-top: 0; }
+.mt-1 { margin-top: 0.5rem; }
+.mt-2 { margin-top: 1rem; }
+.mt-3 { margin-top: 1.5rem; }
+
+/* Responsive Improvements */
+@media (max-width: 768px) {
+ .checkbox-label {
+ margin-top: 0.75rem;
+ margin-bottom: 0.75rem;
+ min-height: 28px;
+ }
+
+ .checkbox-custom {
+ width: 20px;
+ height: 20px;
+ margin-right: 0.75rem;
+ }
+
+ .checkbox-label input[type="checkbox"]:checked + .checkbox-custom::after {
+ font-size: 11px;
+ }
+
+ .items-tree .tree-item input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ }
+
+ .form-group label.checkbox-label {
+ margin-bottom: 1rem;
+ }
+}
diff --git a/synapse-electron/start.bat b/synapse-electron/start.bat
new file mode 100644
index 000000000..ce1ffe4f9
--- /dev/null
+++ b/synapse-electron/start.bat
@@ -0,0 +1,64 @@
+@echo off
+REM Synapse Desktop Client - Windows Startup Script
+
+echo Starting Synapse Desktop Client...
+echo.
+
+REM Check if Node.js is installed
+where node >nul 2>nul
+if %ERRORLEVEL% NEQ 0 (
+ echo ERROR: Node.js is not installed or not in PATH
+ echo Please install Node.js from https://nodejs.org/
+ pause
+ exit /b 1
+)
+
+REM Check if Python is installed
+where python >nul 2>nul
+if %ERRORLEVEL% NEQ 0 (
+ echo ERROR: Python is not installed or not in PATH
+ echo Please install Python from https://python.org/
+ pause
+ exit /b 1
+)
+
+REM Change to the synapse-electron directory
+cd /d %~dp0
+
+REM Check if node_modules exists
+if not exist "node_modules" (
+ echo Installing Node.js dependencies...
+ npm install
+ if %ERRORLEVEL% NEQ 0 (
+ echo ERROR: Failed to install Node.js dependencies
+ pause
+ exit /b 1
+ )
+)
+
+REM Check if Python backend dependencies are installed
+cd backend
+if not exist "venv" (
+ echo Creating Python virtual environment...
+ python -m venv venv
+ call venv\Scripts\activate
+ echo Installing Python dependencies...
+ cd ..
+ pip install -e .[electron]
+ cd backend
+) else (
+ call venv\Scripts\activate
+)
+
+cd ..
+
+REM Start the application
+echo.
+echo Starting Synapse Desktop Client...
+echo Backend will start on http://localhost:8000
+echo WebSocket will start on ws://localhost:8001
+echo.
+
+npm run dev
+
+pause
diff --git a/synapse-electron/start.sh b/synapse-electron/start.sh
new file mode 100755
index 000000000..06f9b6ca0
--- /dev/null
+++ b/synapse-electron/start.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+# Synapse Desktop Client - Unix Startup Script
+
+echo "Starting Synapse Desktop Client..."
+echo
+
+# Check if Node.js is installed
+if ! command -v node &> /dev/null; then
+ echo "ERROR: Node.js is not installed or not in PATH"
+ echo "Please install Node.js from https://nodejs.org/"
+ exit 1
+fi
+
+# Check if Python is installed
+if ! command -v python3 &> /dev/null; then
+ if ! command -v python &> /dev/null; then
+ echo "ERROR: Python is not installed or not in PATH"
+ echo "Please install Python from https://python.org/"
+ exit 1
+ fi
+ PYTHON_CMD="python"
+else
+ PYTHON_CMD="python3"
+fi
+
+# Change to the synapse-electron directory
+cd "$(dirname "$0")"
+
+# Check if node_modules exists
+if [ ! -d "node_modules" ]; then
+ echo "Installing Node.js dependencies..."
+ npm install
+ if [ $? -ne 0 ]; then
+ echo "ERROR: Failed to install Node.js dependencies"
+ exit 1
+ fi
+fi
+
+# Check if Python backend dependencies are installed
+cd backend
+
+# Note: Dependencies are now installed via setup.cfg electron extras
+cd ..
+cd ..
+
+# Activate uv virtual environment and install electron dependencies
+echo "Installing Python dependencies with uv..."
+source .venv/bin/activate
+uv pip install -e .[electron]
+
+cd synapse-electron
+cd backend
+
+# if [ ! -d "venv" ]; then
+# echo "Creating Python virtual environment..."
+# $PYTHON_CMD -m venv venv
+# source venv/bin/activate
+# echo "Installing Python dependencies..."
+# pip install -r requirements.txt
+# cd ..
+# pip install -e .
+# cd backend
+# else
+# source venv/bin/activate
+# fi
+
+cd ..
+
+# Start the application
+echo
+echo "Starting Synapse Desktop Client..."
+echo "Backend will start on http://localhost:8000"
+echo "WebSocket will start on ws://localhost:8001"
+echo
+
+# Check if we're running in a headless environment (no DISPLAY set)
+if [ -z "$DISPLAY" ]; then
+ echo "No display detected - starting virtual display for headless operation..."
+
+ # Start Xvfb (X Virtual Framebuffer) in the background
+ export DISPLAY=:99
+ Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
+ XVFB_PID=$!
+
+ # Give Xvfb a moment to start
+ sleep 2
+
+ echo "Virtual display started on DISPLAY=$DISPLAY"
+
+ # Function to cleanup Xvfb on exit
+ cleanup() {
+ echo "Cleaning up virtual display..."
+ kill $XVFB_PID 2>/dev/null
+ exit
+ }
+
+ # Set trap to cleanup on script exit
+ trap cleanup EXIT INT TERM
+fi
+
+npm run dev
diff --git a/synapseclient/client.py b/synapseclient/client.py
index a6013b152..b5d5c15ed 100644
--- a/synapseclient/client.py
+++ b/synapseclient/client.py
@@ -345,6 +345,7 @@ def __init__(
cache_client: bool = True,
user_agent: Union[str, List[str]] = None,
http_timeout_seconds: int = 70,
+ silent_progress_bars: bool = None,
) -> "Synapse":
"""
Initialize Synapse object
@@ -361,6 +362,8 @@ def __init__(
when making http requests.
cache_root_dir: Root directory for storing cache data.
silent: Suppresses message.
+ silent_progress_bars: Suppresses tqdm progress bars without silencing all output.
+ Useful for packaged applications where progress bars may cause errors.
requests_session_async_synapse: The HTTPX Async client for interacting with
Synapse services.
requests_session_storage: The HTTPX client for interacting with
@@ -450,6 +453,7 @@ def __init__(
self.user_agent = user_agent
self.silent = silent
+ self.silent_progress_bars = silent_progress_bars
self._init_logger() # initializes self.logger
self.skip_checks = skip_checks
diff --git a/synapseclient/core/transfer_bar.py b/synapseclient/core/transfer_bar.py
index cd87c0336..91f20d29b 100644
--- a/synapseclient/core/transfer_bar.py
+++ b/synapseclient/core/transfer_bar.py
@@ -148,7 +148,7 @@ def get_or_create_download_progress_bar(
syn = Synapse.get_client(synapse_client=synapse_client)
- if syn.silent:
+ if syn.silent or syn.silent_progress_bars:
return None
transfer_count: int = getattr(_thread_local, "transfer_count", 0)
diff --git a/synapseclient/core/upload/multipart_upload.py b/synapseclient/core/upload/multipart_upload.py
index f216a305e..5be34cdc5 100644
--- a/synapseclient/core/upload/multipart_upload.py
+++ b/synapseclient/core/upload/multipart_upload.py
@@ -480,7 +480,7 @@ def multipart_upload_file(
total=file_size,
leave=None,
)
- if not syn.silent
+ if not syn.silent and not syn.silent_progress_bars
else None
)
md5_hex = md5_for_file(file_path, progress_bar=progress_bar).hexdigest()
diff --git a/synapseclient/core/upload/multipart_upload_async.py b/synapseclient/core/upload/multipart_upload_async.py
index b078e2797..4c7abaaf7 100644
--- a/synapseclient/core/upload/multipart_upload_async.py
+++ b/synapseclient/core/upload/multipart_upload_async.py
@@ -375,7 +375,11 @@ async def _upload_parts(
asyncio.create_task(self._handle_part_wrapper(part_number=part_number))
)
- if not self._syn.silent and not self._progress_bar:
+ if (
+ not self._syn.silent
+ and not self._progress_bar
+ and not self._syn.silent_progress_bars
+ ):
if self._is_copy():
# we won't have bytes to measure during a copy so the byte oriented
# progress bar is not useful