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 `
${menuItem.label}
`; + }).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 + + + + + + + + +
+
+
+ +

Synapse Desktop Client

+
+
+
+ +
+ + + + + +
+
+ + + + 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