diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d4a2c44 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.bat] +indent_style = tab +end_of_line = crlf + +[LICENSE] +insert_final_newline = false + +[Makefile] +indent_style = tab diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b441d59 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# ComfyUI-SwissArmyKnife Environment Variables +# Copy this file to .env and fill in your actual API keys + +# Google Gemini API Key +# Get your API key from: https://aistudio.google.com/app/apikey +GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE + +# Optional: ComfyUI Registry Access Token (for publishing) +# Only needed if you plan to publish to ComfyUI Registry +REGISTRY_ACCESS_TOKEN=YOUR_REGISTRY_TOKEN_HERE \ No newline at end of file diff --git a/.github/COPILOT_INSTRUCTIONS.md b/.github/COPILOT_INSTRUCTIONS.md new file mode 100644 index 0000000..52d7ca0 --- /dev/null +++ b/.github/COPILOT_INSTRUCTIONS.md @@ -0,0 +1,260 @@ +# ComfyUI-SwissArmyKnife - Copilot Coding Agent Instructions + +This file provides specific guidance for the GitHub Copilot coding agent when working on the ComfyUI-SwissArmyKnife repository. + +## Quick Reference + +### Repository Structure + +- **Python Backend**: `utils/nodes.py`, `utils/helper_nodes.py` - Gemini AI video/image analysis nodes +- **JavaScript Web Extension**: `web/js/gemini_widgets.js`, `web/css/gemini_widgets.css` - ComfyUI widgets +- **Dev Container**: `.devcontainer/` - GPU-enabled development environment using `mmartial/comfyui-nvidia-docker` +- **Configuration**: `pyproject.toml` - Python project config with dev dependencies + +### Safe Agent Commands + +**Always safe and fast (< 5 seconds):** + +```bash +# Python linting +ruff check . +ruff check --fix . + +# Test Python imports +python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('✓ Nodes:', list(NODE_CLASS_MAPPINGS.keys()))" + +# JavaScript linting +cd web && npm run lint + +# Check file structure +ls -la web/js/ && ls -la web/css/ +``` + +**Dependency installation (set timeouts appropriately):** + +```bash +# Basic Python setup (12 seconds) +pip install -e . + +# ML dependencies (2-3 minutes - SET TIMEOUT 300+ seconds) +pip install opencv-python pillow + +# Dev tools (30 seconds) +pip install ruff black mypy pytest + +# Node.js dependencies (40 seconds - SET TIMEOUT 60+ seconds) +cd web && npm install +``` + +**Testing:** + +```bash +# Python tests (< 30 seconds) +pytest + +# JavaScript tests (requires running ComfyUI server) +cd web/tests && npm test +``` + +## Development Environment + +### Dev Container Setup + +The repository includes a complete dev container configuration: + +- **Image**: `mmartial/comfyui-nvidia-docker:ubuntu24_cuda12.8-latest` +- **GPU Support**: `--gpus all` for CUDA acceleration +- **Auto Setup**: `.devcontainer/postCreate.sh` installs all dependencies +- **ComfyUI Integration**: Repository is symlinked to `/opt/ComfyUI/custom_nodes/comfyui_swissarmyknife` + +### Local Development Commands + +```bash +# Start dev container setup +./.devcontainer/postCreate.sh + +# Start ComfyUI with custom nodes +./.devcontainer/run-comfy.sh +# → Available at http://localhost:8188 +``` + +## Code Quality & Linting + +### Python (Backend) + +```bash +# Lint (very fast < 1 second) +ruff check . + +# Auto-fix issues +ruff check --fix . + +# Format code +black . + +# Type checking +mypy . +``` + +**Expected warnings to ignore:** + +- Unused imports in `__init__.py` files (intentional for ComfyUI integration) +- Some import-related warnings in UI backup files + +### JavaScript (Web Extension) + +```bash +cd web + +# Lint JavaScript +npm run lint +npm run lint:fix + +# Format code +npm run format +``` + +**No build step required** - JavaScript widgets are plain JS files loaded directly by ComfyUI. + +## Testing Strategy + +### Python Backend Testing + +```bash +# Verify nodes load correctly +python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('Available nodes:', list(NODE_CLASS_MAPPINGS.keys()))" + +# Expected output: ['GeminiUtilVideoDescribe', 'GeminiUtilImageDescribe', 'GeminiUtilMediaDescribe'] + +# Run existing tests +pytest +``` + +### JavaScript Web Extension Testing + +```bash +# Playwright tests (requires running ComfyUI server) +cd web/tests +npm test +``` + +### System Dependencies Validation + +```bash +# Verify FFmpeg (required for video processing) +ffmpeg -version | head -1 + +# Verify Python ML libraries +python -c "import cv2, PIL; print('✓ OpenCV and PIL available')" +``` + +## File Change Guidelines + +### Python Changes + +- **Scope**: Modify files in `utils/` directory only for backend changes +- **Restart Required**: ComfyUI server must be restarted after Python changes +- **Testing**: Always run `python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print(list(NODE_CLASS_MAPPINGS.keys()))"` after changes + +### JavaScript Changes + +- **Scope**: Modify files in `web/js/` and `web/css/` directories +- **No Build Step**: Files are loaded directly by ComfyUI +- **Testing**: Browser cache refresh required to see changes in ComfyUI + +### Configuration Changes + +- `pyproject.toml`: Python project metadata, dependencies, and tool configuration +- `web/package.json`: JavaScript development dependencies and scripts +- `.devcontainer/`: Development environment configuration + +## CI/CD Integration + +### GitHub Actions + +- **Static Analysis Only**: CI does not require GPU - runs linting, type checking, and tests +- **Automatic Publishing**: Updates to `pyproject.toml` trigger publishing to ComfyUI Registry +- **Required Secret**: `REGISTRY_ACCESS_TOKEN` for registry publishing + +### Pre-commit Hooks + +```bash +# Install hooks +pre-commit install + +# Run manually on all files +pre-commit run --all-files + +# Skip for emergency commits +git commit --no-verify +``` + +## Architecture Notes + +### ComfyUI Integration Points + +- **Backend Entry**: `__init__.py` exports `NODE_CLASS_MAPPINGS`, `NODE_DISPLAY_NAME_MAPPINGS`, `WEB_DIRECTORY` +- **Web Directory**: `WEB_DIRECTORY = "./web"` tells ComfyUI where to find JavaScript/CSS files +- **Node Registration**: Python nodes are automatically loaded via the mappings +- **Widget Registration**: JavaScript widgets register via `app.registerExtension()` + +### Current Node Implementations + +- **GeminiUtilVideoDescribe**: Video analysis using Google Gemini AI +- **GeminiUtilImageDescribe**: Image analysis using Google Gemini AI +- **GeminiUtilMediaDescribe**: Multi-media analysis functionality +- **Helper Nodes**: Additional utility nodes in `utils/helper_nodes.py` + +## Performance Notes + +### Expected Command Timing + +- `ruff check .`: < 1 second (very fast) +- `pip install -e .`: ~12 seconds (basic dependencies) +- `pip install opencv-python`: 2-3 minutes (large ML binaries) +- `npm install`: ~40 seconds (JavaScript dependencies) +- `pytest`: < 30 seconds (minimal test suite) + +### Memory and GPU Usage + +- **Development**: GPU passthrough via dev container for testing +- **CI/CD**: No GPU required - static analysis only +- **Production**: GPU acceleration available for Gemini AI processing + +## Troubleshooting + +### Common Issues + +1. **Python Import Errors**: Missing dependencies (opencv-python, pillow, google-genai) +2. **JavaScript Widget Not Loading**: Browser cache needs refresh after changes +3. **ComfyUI Not Finding Nodes**: Check symlink in `/opt/ComfyUI/custom_nodes/comfyui_swissarmyknife` +4. **FFmpeg Not Found**: Install via `apt-get install ffmpeg` or similar + +### Debug Commands + +```bash +# Check Python environment +python -c "import sys; print('Python path:', sys.path)" + +# Verify symlink in dev container +ls -la /opt/ComfyUI/custom_nodes/comfyui_swissarmyknife + +# Check ComfyUI web directory registration +python -c "import __init__; print('WEB_DIRECTORY:', __init__.WEB_DIRECTORY)" +``` + +## Agent Workflow Recommendations + +1. **Start with linting**: Always run `ruff check .` first to understand current state +2. **Test imports early**: Verify Python nodes load before making changes +3. **Make minimal changes**: Focus on surgical modifications rather than large refactors +4. **Test incrementally**: Run tests after each logical change +5. **Use pre-commit**: Let automation catch style issues +6. **Document changes**: Update this file if workflow changes + +## References + +- [ComfyUI Custom Node Documentation](https://docs.comfy.org/custom-nodes/overview) +- [mmartial/ComfyUI-Nvidia-Docker](https://github.com/mmartial/ComfyUI-Nvidia-Docker) +- [GitHub Copilot Agent Customization](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/customize-the-agent-environment) +- [Dev Container Specification](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..516ad50 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,377 @@ +# ComfyUI-SwissArmyKnife - ComfyUI Extension Development Guide + +**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.** + +## Official ComfyUI Documentation + +For technical implementation details, reference these official ComfyUI custom node development guides: + +- **Backend (Python) Development**: https://docs.comfy.org/custom-nodes/backend/server_overview +- **Web Extension (JavaScript) Development**: https://docs.comfy.org/custom-nodes/js/javascript_overview + +_Note: These are the official JavaScript widget docs, not React-based extensions._ + +## Project Overview + +ComfyUI-SwissArmyKnife is a ComfyUI extension that consists of two essential components that work together: + +### Backend (Python Custom Nodes) + +- Python custom nodes in `utils/*.py` for Gemini AI video analysis and processing +- Core business logic and AI integration +- Requires ComfyUI server restart when modified + +### Web Extension (JavaScript Widgets) + +- Plain JavaScript widgets in `./web/js/` for enhanced ComfyUI interaction +- UI components and client-side functionality +- Requires browser cache refresh when modified + +### Additional Components + +- ~~React/TypeScript UI extension with internationalization~~ **DISABLED FOR NOW** +- GitHub Actions for automated building and publishing +- Playwright tests in `./web/tests/` for testing against hosted ComfyUI server + +**IMPORTANT**: Both backend and web extension components are required for the custom node to function properly. Changes to backend Python files require restarting the ComfyUI server, while changes to web JavaScript files require refreshing the browser cache. + +**Note: The React UI component (`ui-react_backup/`) is currently disabled and not in active development.** + +## Working Effectively + +### Essential System Dependencies + +Install these system dependencies first: + +```bash +# FFmpeg is REQUIRED for video processing functionality +sudo apt-get update && sudo apt-get install -y ffmpeg + +# Verify installation +ffmpeg -version # Should show version 6.1.1 or newer +``` + +### Bootstrap React UI Development _(DISABLED)_ + +**⚠️ The React UI component is currently disabled and not in active development.** + +```bash +# THESE COMMANDS ARE CURRENTLY DISABLED +# Navigate to React UI directory +# cd ui-react_backup/ui + +# Install Node.js dependencies - takes ~40 seconds +# npm install # NEVER CANCEL: Takes 40 seconds. Set timeout to 60+ seconds. + +# Build React extension - takes ~5 seconds +# npm run build # NEVER CANCEL: Takes 5 seconds. Set timeout to 30+ seconds. +``` + +### Bootstrap Python Development + +```bash +# Install Python package and dependencies +pip3 install -e . # Takes ~12 seconds for basic dependencies + +# Install essential Python dependencies for full functionality +# NEVER CANCEL: Takes 2-3 minutes for torch/opencv. Set timeout to 300+ seconds. +pip3 install torch opencv-python + +# Install development tools +pip3 install pytest mypy coverage ruff pre-commit + +# Verify Python modules work +python3 -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('Available nodes:', list(NODE_CLASS_MAPPINGS.keys()))" +# Should output: Available nodes: ['GeminiUtilVideoDescribe'] +``` + +## Development Workflows + +### React UI Development _(DISABLED)_ + +**⚠️ The React UI component is currently disabled and not in active development.** + +```bash +# THESE COMMANDS ARE CURRENTLY DISABLED +# cd ui-react_backup/ui + +# Development with auto-reload (watches for changes) +# npm run watch + +# Build for production +# npm run build # NEVER CANCEL: Takes 5 seconds. Set timeout to 30+ seconds. + +# TypeScript type checking +# npm run typecheck +``` + +### Backend (Python) Development + +```bash +# Import and test Python nodes +python3 -c "from utils.nodes import NODE_CLASS_MAPPINGS; print(list(NODE_CLASS_MAPPINGS.keys()))" + +# Run Python linting (fast, < 1 second) +ruff check . + +# Fix auto-fixable linting issues +ruff check --fix . +``` + +### Web Extension (JavaScript) Development + +```bash +# JavaScript widgets are located in ./web/js/ +# No build step required - plain JavaScript files + +# Verify web extension files exist +ls -la web/js/gemini_widgets.js +ls -la web/css/gemini_widgets.css + +# After making changes to JavaScript widgets: +# 1. No compilation needed (plain JS) +# 2. Browser cache refresh required for changes to take effect +``` + +## Testing + +### Backend (Python) Testing + +```bash +# Run pytest (currently no tests exist) +pytest # NEVER CANCEL: Takes < 30 seconds. Set timeout to 60+ seconds. +# Expected: "no tests ran" - this is normal, no tests exist yet + +# When adding tests, create them in a tests/ directory +``` + +### Web Extension Testing + +```bash +# Playwright tests for testing against hosted ComfyUI server +cd web/tests/ + +# Install Playwright dependencies (if not already installed) +npm install + +# Run Playwright tests against hosted ComfyUI server +npm test + +# Note: Tests require a running ComfyUI server with the custom node installed +``` + +### React UI Testing _(DISABLED)_ + +**⚠️ The React UI component is currently disabled and not in active development.** + +```bash +# THESE COMMANDS ARE CURRENTLY DISABLED +# cd ui-react_backup/ui + +# This command WILL FAIL with ES module errors +# npm test +# Error: "Jest encountered an unexpected token" - this is a known issue + +# To add working tests, fix jest.config.js ES module configuration first +``` + +## Linting and Code Quality + +### Python Linting (Works) + +```bash +# Fast linting check (< 1 second) +ruff check . + +# Fix automatically fixable issues +ruff check --fix . + +# Note: Some unused import warnings are expected and can be ignored +# The code functions correctly despite these warnings +``` + +### React UI Linting _(DISABLED)_ + +**⚠️ The React UI component is currently disabled and not in active development.** + +```bash +# THESE COMMANDS ARE CURRENTLY DISABLED +# cd ui-react_backup/ui + +# This will show configuration errors but runs +# npm run lint # Shows ESLint TypeScript project configuration issues + +# Auto-fix formatting issues +# npm run lint:fix + +# Format code +# npm run format +``` + +## Build and Publish Workflow + +### Local Build Process _(DISABLED)_ + +**⚠️ The React UI component is currently disabled and not in active development.** + +```bash +# THESE COMMANDS ARE CURRENTLY DISABLED +# Complete build from scratch +# cd ui-react_backup/ui +# npm install # 40 seconds +# npm run build # 5 seconds + +# Verify built files exist +# ls -la ../dist/example_ext/ # Should show main.js, main.css, etc. +``` + +### GitHub Actions Publishing + +The repository includes automated publishing via `.github/workflows/react-build.yml`: + +- Triggers on `pyproject.toml` changes pushed to main branch +- Requires `REGISTRY_ACCESS_TOKEN` secret in repository settings +- Automatically builds React UI and publishes to ComfyUI Registry + +## Validation Scenarios + +### Always validate changes by running these verification steps: + +#### Python Node Validation + +```bash +# 1. Import test +python3 -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('✓ Python imports work')" + +# 2. Linting +ruff check . # Accept unused import warnings as normal + +# 3. Verify system dependencies +ffmpeg -version | head -1 # Should show FFmpeg version +``` + +#### React UI Validation _(DISABLED)_ + +**⚠️ The React UI component is currently disabled and not in active development.** + +```bash +# THESE COMMANDS ARE CURRENTLY DISABLED +# cd ui-react_backup/ui + +# 1. Build test +# npm run build # Should complete in ~5 seconds + +# 2. Verify outputs +# ls -la ../dist/example_ext/main.js # Should exist +# ls -la ../dist/locales/ # Should contain en/ and zh/ directories + +# 3. TypeScript check +# npm run typecheck # Should pass without errors +``` + +#### Full Integration Test _(PARTIALLY DISABLED)_ + +**⚠️ React UI components are disabled, but JavaScript widgets remain active.** + +```bash +# 1. Clean build (DISABLED - React components only) +# cd ui-react_backup/ui +# rm -rf ../dist/ +# npm run build + +# 2. Verify ComfyUI integration files +# ls -la ../dist/example_ext/ # React build outputs (DISABLED) +ls -la ../../web/js/ # JavaScript widgets (ACTIVE) +ls -la ../../__init__.py # Python entry point (ACTIVE) +``` + +## Known Issues and Limitations + +### Jest Testing Configuration + +- Jest tests fail due to ES module configuration issues +- Example test exists in `src/__tests__/dummy.test.tsx` but won't run +- Fix requires updating jest.config.js and jest.setup.js for ESM support + +### ESLint Configuration + +- ESLint shows TypeScript project configuration warnings +- Code builds and runs correctly despite these warnings +- Focus on functional validation rather than linting perfection + +### Python Import Warnings + +- Ruff reports unused imports in `__init__.py` files +- These imports are intentional for ComfyUI integration +- The warnings can be safely ignored + +### System Dependencies + +- FFmpeg is required for video processing features +- Without FFmpeg, video-related functionality will fail at runtime +- Always verify FFmpeg is installed in your environment + +## Common Tasks + +### File Structure Overview + +``` +comfyui_swissarmyknife/ +├── .github/workflows/react-build.yml # CI/CD automation +├── __init__.py # Python entry point +├── pyproject.toml # Project metadata & publishing config +├── utils/nodes.py # Main Python custom nodes +├── web/js/gemini_widgets.js # JavaScript widgets +└── ui-react_backup/ # React UI extension + ├── ui/src/ # React source code + ├── ui/package.json # Node.js dependencies + └── dist/ # Built React outputs +``` + +### Package Management + +```bash +# React dependencies +cd ui-react_backup/ui +npm install # Install from package.json + +# Python dependencies +pip3 install -e . # Install from pyproject.toml +pip3 install torch opencv-python # Additional ML dependencies +``` + +### Environment Verification + +```bash +# Verify tool versions +node --version # Should be v20.19.4+ +npm --version # Should be 10.8.2+ +python3 --version # Should be 3.12.3+ +ffmpeg -version # Should be 6.1.1+ + +# Verify dependencies +npm list --prefix ui-react_backup/ui # Show React dependencies +pip3 list | grep -E "(torch|opencv|google-genai)" # Show Python ML deps +``` + +## Timing Expectations + +**NEVER CANCEL these operations - they may appear to hang but are working:** + +- `npm install`: 40 seconds (normal) +- `npm run build`: 5 seconds (very fast) +- `pip3 install torch opencv-python`: 2-3 minutes (downloads large binaries) +- `npm run watch`: Runs continuously until stopped +- `ruff check`: < 1 second (very fast) +- `pytest`: < 30 seconds (but no tests exist currently) + +Set appropriate timeouts: 60+ seconds for npm install, 300+ seconds for Python ML dependencies. + +## Development Tips + +- **Backend changes**: Restart ComfyUI server after modifying Python files in `utils/` +- **Web extension changes**: Refresh browser cache after modifying JavaScript files in `web/js/` +- JavaScript widgets require no build step (plain JS files) +- Use Playwright tests in `web/tests/` to validate functionality against hosted ComfyUI server +- Focus on functional testing since automated tests have configuration issues +- FFmpeg must be available in PATH for video processing features to work diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..73a6c7c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,160 @@ +name: CI - Lint, Build, and Test + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + +jobs: + python-quality: + name: Python Linting and Testing + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install ruff black mypy pytest coverage opencv-python pillow + + - name: Lint with ruff + run: | + ruff check . + + - name: Check formatting with black + run: | + black --check . + + - name: Type check with mypy + run: | + mypy . + + - name: Test Python imports + run: | + python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('✓ Python nodes loaded:', list(NODE_CLASS_MAPPINGS.keys()))" + + - name: Run tests with pytest + run: | + pytest --cov=utils --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: matrix.python-version == '3.12' + with: + file: ./coverage.xml + fail_ci_if_error: false + + javascript-quality: + name: JavaScript Linting + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web/package.json + + - name: Install JavaScript dependencies + run: | + cd web + npm ci + + - name: Lint JavaScript + run: | + cd web + npm run lint + + - name: Check JavaScript formatting + run: | + cd web + npm run format -- --check + + integration-test: + name: Integration Test (No GPU) + runs-on: ubuntu-latest + needs: [python-quality, javascript-quality] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install opencv-python pillow + + - name: Verify project structure + run: | + echo "✓ Checking project structure..." + test -f "__init__.py" || exit 1 + test -f "utils/nodes.py" || exit 1 + test -f "web/js/gemini_widgets.js" || exit 1 + test -d ".devcontainer" || exit 1 + echo "✓ All required files present" + + - name: Test ComfyUI integration points + run: | + python -c " + import __init__ + assert hasattr(__init__, 'NODE_CLASS_MAPPINGS'), 'NODE_CLASS_MAPPINGS not exported' + assert hasattr(__init__, 'NODE_DISPLAY_NAME_MAPPINGS'), 'NODE_DISPLAY_NAME_MAPPINGS not exported' + assert hasattr(__init__, 'WEB_DIRECTORY'), 'WEB_DIRECTORY not exported' + assert __init__.WEB_DIRECTORY == './web', f'WEB_DIRECTORY should be ./web, got {__init__.WEB_DIRECTORY}' + print('✓ ComfyUI integration points verified') + " + + - name: Verify FFmpeg availability + run: | + ffmpeg -version | head -1 + echo "✓ FFmpeg is available" + + pre-commit: + name: Pre-commit Hooks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install pre-commit + run: | + python -m pip install --upgrade pip + pip install pre-commit + + - name: Run pre-commit hooks + run: | + pre-commit run --all-files \ No newline at end of file diff --git a/.github/workflows/react-build.yml b/.github/workflows/react-build.yml new file mode 100644 index 0000000..0ba5ecf --- /dev/null +++ b/.github/workflows/react-build.yml @@ -0,0 +1,40 @@ +name: React Extension Build + Publish to Comfy Registry + +on: + push: + branches: [ "main" ] + paths: + - 'pyproject.toml' + +jobs: + build: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'Comfy-Org' }} + + strategy: + matrix: + node-version: [18.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: | + cd ui + npm install + + - name: Build React extension + run: | + cd ui + npm run build + + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} + skip_checkout: 'true' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41e0284 --- /dev/null +++ b/.gitignore @@ -0,0 +1,152 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# OSX useful to ignore +*.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# IntelliJ Idea +.idea +*.iml +*.ipr +*.iws + +# PyBuilder +target/ + +# Cookiecutter +output/ +python_boilerplate/ +cookiecutter-pypackage-env/ + +# vscode settings +.history/ +*.code-workspace + +# Frontend extension +node_modules/ +node_modules +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +node.zip +.vscode/ +.claude/ + +# Dev container and development files +.devcontainer/.env +temp/ +input/ +output/ +models/ +.ruff_cache/ + +# Gemini description cache +cache/ + +# OS and editor files +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# Environment variables and API keys - SECURITY +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# API keys and secrets - SECURITY +*.key +*.pem +secrets.json +config.json +api_keys.txt +*_secrets.* + +# Cache directories +cache/ +.cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2539924 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.4.9 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fd01797 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,287 @@ +# Contributing to ComfyUI-SwissArmyKnife + +Welcome to ComfyUI-SwissArmyKnife! This document provides guidelines for contributors and the Copilot coding agent. + +## Development Environment Setup + +### Dev Container (Recommended) + +The easiest way to get started is using the provided dev container with GPU support: + +1. **Prerequisites:** + + - Docker with NVIDIA GPU support + - VS Code with Dev Containers extension + - NVIDIA GPU drivers + +2. **Quick Start:** + + ```bash + # Open in VS Code and use "Reopen in Container" + # OR run directly with Docker: + docker run --gpus all -v $(pwd):/workspaces/repo -p 8188:8188 mmartial/comfyui-nvidia-docker:ubuntu24_cuda12.8-latest + ``` + +3. **Start ComfyUI:** + ```bash + ./.devcontainer/run-comfy.sh + # ComfyUI will be available at http://localhost:8188 + ``` + +### Manual Setup + +If you prefer to set up locally: + +1. **Install Python dependencies:** + + ```bash + pip install -e . + pip install ruff black mypy pytest opencv-python pillow + ``` + +2. **Install JavaScript tooling:** + + ```bash + cd web + npm install + ``` + +3. **Install FFmpeg:** + + ```bash + # Ubuntu/Debian + sudo apt-get install ffmpeg + + # macOS + brew install ffmpeg + ``` + +## Development Workflow + +### Python Backend Development + +The backend nodes are located in `utils/`: + +- `utils/nodes.py` - Main Gemini AI integration nodes +- `utils/helper_nodes.py` - Helper/utility nodes + +**Development commands:** + +```bash +# Lint Python code +ruff check . +ruff check --fix . # Auto-fix issues + +# Format Python code +black . + +# Type checking +mypy . + +# Run tests +pytest + +# Test imports work +python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('Nodes:', list(NODE_CLASS_MAPPINGS.keys()))" +``` + +### JavaScript Web Extension Development + +The web extension consists of plain JavaScript widgets in `web/`: + +- `web/js/gemini_widgets.js` - Main widget implementation +- `web/css/gemini_widgets.css` - Widget styling + +**No build step required** - files are loaded directly by ComfyUI. + +**Development commands:** + +```bash +cd web + +# Lint JavaScript +npm run lint +npm run lint:fix # Auto-fix issues + +# Format code +npm run format + +# Test (Playwright tests) +cd tests +npm test +``` + +**Important:** After making changes to JavaScript files, refresh the browser cache in ComfyUI to see changes. + +### Pre-commit Hooks + +This project uses pre-commit hooks to maintain code quality: + +```bash +# Install hooks +pre-commit install + +# Run manually +pre-commit run --all-files + +# Skip hooks (for testing) +git commit --no-verify +``` + +## Testing + +### Python Testing + +```bash +pytest # Run all Python tests +``` + +### JavaScript Testing + +```bash +cd web/tests +npm test # Run Playwright tests against hosted ComfyUI +``` + +**Note:** JavaScript tests require a running ComfyUI server with the custom node installed. + +## Code Style Guidelines + +### Python + +- Use `ruff` for linting (configured in `pyproject.toml`) +- Use `black` for formatting +- Follow type hints with `mypy` +- Maximum line length: 140 characters + +### JavaScript + +- Use ESLint for linting +- Use Prettier for formatting +- Follow ComfyUI widget conventions +- No build step - plain JavaScript files + +## Project Structure + +``` +comfyui_swissarmyknife/ +├── .devcontainer/ # Dev container configuration +│ ├── devcontainer.json # Main dev container config +│ ├── postCreate.sh # Setup script +│ └── run-comfy.sh # ComfyUI start script +├── utils/ # Python backend nodes +│ ├── nodes.py # Main Gemini AI nodes +│ └── helper_nodes.py # Helper nodes +├── web/ # JavaScript web extension +│ ├── js/ # Widget JavaScript files +│ ├── css/ # Widget CSS files +│ ├── tests/ # Playwright tests +│ └── package.json # Node.js dependencies +├── __init__.py # ComfyUI entry point +├── pyproject.toml # Python project configuration +└── README.md # Project documentation +``` + +## ComfyUI Integration + +### Backend Nodes + +- Exported via `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` in `__init__.py` +- Require ComfyUI server restart after changes + +### Web Extension + +- Loaded via `WEB_DIRECTORY = "./web"` in `__init__.py` +- Changes require browser cache refresh only + +## CI/CD + +### Continuous Integration + +- Automated linting and testing via GitHub Actions +- **No GPU required** for CI - only static analysis +- Runs on Python and JavaScript code changes + +### Publishing + +- Automatic publishing to ComfyUI Registry via GitHub Actions +- Triggered by `pyproject.toml` version updates +- Requires `REGISTRY_ACCESS_TOKEN` secret + +## Common Tasks + +### Adding a New Python Node + +1. Add node class to `utils/nodes.py` or `utils/helper_nodes.py` +2. Export in `NODE_CLASS_MAPPINGS` +3. Add tests in `tests/` directory +4. Update documentation + +### Adding JavaScript Widgets + +1. Add widget code to `web/js/` +2. Add styling to `web/css/` +3. Register with ComfyUI using `app.registerExtension()` +4. Test with browser refresh + +### Running Local Tests + +```bash +# Python +python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('Available nodes:', list(NODE_CLASS_MAPPINGS.keys()))" +ruff check . +pytest + +# JavaScript +cd web +npm run lint +cd tests && npm test # Requires running ComfyUI server +``` + +## Copilot Coding Agent Guidelines + +For the GitHub Copilot coding agent working on this repository: + +### Safe Commands to Run + +```bash +# Linting (fast, safe) +ruff check . +ruff check --fix . + +# Testing imports (fast) +python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print(list(NODE_CLASS_MAPPINGS.keys()))" + +# JavaScript linting +cd web && npm run lint + +# Run tests +pytest +cd web/tests && npm test +``` + +### Expected Timeouts + +- `ruff check .`: < 1 second +- `pip install -e .`: ~12 seconds +- `pip install opencv-python`: 2-3 minutes (large downloads) +- `npm install`: ~40 seconds +- `pytest`: < 30 seconds + +### Key Development Points + +- Backend changes require ComfyUI server restart +- Web extension changes require browser cache refresh only +- JavaScript widgets are plain JS files (no build step) +- Focus on minimal, surgical changes +- Use pre-commit hooks to maintain quality + +## Getting Help + +- Check existing issues on GitHub +- Review ComfyUI custom node documentation: https://docs.comfy.org/custom-nodes/overview +- Check the project's implementation status in `IMPLEMENTATION_STATUS.md` + +## License + +This project is licensed under the GNU General Public License v3. See `LICENSE` for details. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d634ba0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,675 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. + +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". + +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. + +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + +Copyright (C) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/README.md b/README.md index 3994167..d2b2c56 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,31 @@ -# ComfyUI-SwissKnifeUtils +# ComfyUI-SwissArmyKnife A collection of custom nodes for ComfyUI featuring Gemini AI integration for video and image analysis, with a complete development environment using GPU-accelerated containers. ## Features -- **🤖 Gemini AI Integration**: Video, image, and media analysis using Google's Gemini AI -- **🎮 JavaScript Widgets**: Enhanced ComfyUI interfaces with custom video controls -- **🐳 Dev Container Support**: GPU-enabled development environment with mmartial/ComfyUI-Nvidia-Docker -- **🧪 Full Testing Suite**: Python and JavaScript testing with CI/CD integration -- **⚡ Hot Reload Development**: Instant feedback for both backend and frontend changes +- **🤖 Gemini AI Integration**: Video, image, and media analysis using Google's Gemini AI +- **🎮 JavaScript Widgets**: Enhanced ComfyUI interfaces with custom video controls +- **🐳 Dev Container Support**: GPU-enabled development environment with mmartial/ComfyUI-Nvidia-Docker +- **🧪 Full Testing Suite**: Python and JavaScript testing with CI/CD integration +- **⚡ Hot Reload Development**: Instant feedback for both backend and frontend changes ## Quick Start with Dev Container (Recommended) The fastest way to get started is using the provided dev container with GPU support: ### Prerequisites -- Docker with NVIDIA Container Toolkit -- VS Code with Dev Containers extension -- NVIDIA GPU with drivers installed + +- Docker with NVIDIA Container Toolkit +- VS Code with Dev Containers extension +- NVIDIA GPU with drivers installed ### Setup + ```bash # Clone the repository -git clone https://github.com/sammykumar/ComfyUI-SwissKnifeUtils.git -cd ComfyUI-SwissKnifeUtils +git clone https://github.com/sammykumar/ComfyUI-SwissArmyKnife.git +cd ComfyUI-SwissArmyKnife # Open in VS Code code . @@ -50,10 +52,10 @@ If you want to use the nodes in an existing ComfyUI installation: cd ComfyUI/custom_nodes # Clone the repository -git clone https://github.com/sammykumar/ComfyUI-SwissKnifeUtils.git +git clone https://github.com/sammykumar/ComfyUI-SwissArmyKnife.git # Install dependencies -cd ComfyUI-SwissKnifeUtils +cd ComfyUI-SwissArmyKnife pip install -e . # Restart ComfyUI @@ -65,8 +67,8 @@ If you're contributing to the project or want to modify the nodes: ```bash # Clone the repository -git clone https://github.com/sammykumar/ComfyUI-SwissKnifeUtils.git -cd ComfyUI-SwissKnifeUtils +git clone https://github.com/sammykumar/ComfyUI-SwissArmyKnife.git +cd ComfyUI-SwissArmyKnife # Install Python dependencies pip install -e . @@ -81,25 +83,51 @@ pre-commit install ``` **System Requirements:** -- Python 3.10+ -- Node.js 20+ (for development) -- FFmpeg (for video processing) -- NVIDIA GPU (optional, for accelerated inference) + +- Python 3.10+ +- Node.js 20+ (for development) +- FFmpeg (for video processing) +- NVIDIA GPU (optional, for accelerated inference) ## Available Nodes ### Backend Nodes (Python) -- **GeminiUtilVideoDescribe**: Analyze videos using Gemini AI -- **GeminiUtilImageDescribe**: Analyze images using Gemini AI -- **GeminiUtilMediaDescribe**: Multi-media analysis functionality + +- **GeminiUtilVideoDescribe**: Analyze videos using Gemini AI +- **GeminiUtilImageDescribe**: Analyze images using Gemini AI +- **GeminiUtilMediaDescribe**: Multi-media analysis functionality ### Web Extension (JavaScript) -- **Video Controls Widget**: Enhanced video timeline controls with trimming -- **Custom Styling**: Themed UI components that integrate with ComfyUI + +- **Video Controls Widget**: Enhanced video timeline controls with trimming +- **Custom Styling**: Themed UI components that integrate with ComfyUI + +## 🔐 API Key Configuration + +### Required: Google Gemini API Key + +To use the Gemini AI features, you'll need a Google Gemini API key: + +1. **Get your API key**: Visit [Google AI Studio](https://aistudio.google.com/app/apikey) +2. **Create API key**: Click "Create API Key" and copy the generated key +3. **Configure securely**: + + ```bash + # Copy the environment template + cp .env.example .env + + # Edit .env and add your actual API key + GEMINI_API_KEY=your_actual_api_key_here + ``` + +⚠️ **Security Note**: Never commit your actual API keys to version control. The `.env` file is already included in `.gitignore`. + +**Alternative**: You can also enter your API key directly in the ComfyUI node interface, but using environment variables is more secure. ## Development Workflow ### Dev Container (Recommended) + ```bash # Start the development environment ./.devcontainer/run-comfy.sh @@ -109,13 +137,14 @@ pre-commit install ``` ### Local Development + ```bash # Python backend development ruff check . # Lint Python code pytest # Run Python tests python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print(list(NODE_CLASS_MAPPINGS.keys()))" -# JavaScript web extension development +# JavaScript web extension development cd web npm run lint # Lint JavaScript npm run format # Format code @@ -123,19 +152,23 @@ cd tests && npm test # Run Playwright tests (requires running ComfyUI) ``` **Important Notes:** -- Python changes require ComfyUI server restart -- JavaScript changes require browser cache refresh only -- JavaScript widgets are plain JS files (no build step required) + +- Python changes require ComfyUI server restart +- JavaScript changes require browser cache refresh only +- JavaScript widgets are plain JS files (no build step required) ## Testing ### Automated Testing + The project includes comprehensive CI/CD testing: -- **Python**: Linting (ruff), formatting (black), type checking (mypy), unit tests (pytest) -- **JavaScript**: Linting (eslint), formatting (prettier), integration tests (playwright) -- **Integration**: ComfyUI node loading, FFmpeg availability, system compatibility + +- **Python**: Linting (ruff), formatting (black), type checking (mypy), unit tests (pytest) +- **JavaScript**: Linting (eslint), formatting (prettier), integration tests (playwright) +- **Integration**: ComfyUI node loading, FFmpeg availability, system compatibility ### Manual Testing + ```bash # Test Python nodes python -c "from utils.nodes import NODE_CLASS_MAPPINGS; print('Nodes:', list(NODE_CLASS_MAPPINGS.keys()))" @@ -154,6 +187,7 @@ This template includes a simple example extension that displays workflow node st 2. Click to open the example UI When developing your own extension, you can: + 1. Replace the example UI in App.tsx with your own components 2. Update the tab title and icon in main.tsx 3. Customize the extension's appearance and behavior @@ -177,24 +211,24 @@ npm run watch This template provides access to ComfyUI's powerful JavaScript APIs through the official type definitions. You can use these APIs to build rich extensions: -- **Sidebar Tabs**: Create custom sidebar panels like this template demonstrates -- **Bottom Bar Panels**: Add panels to the bottom of the UI -- **Top Menu Items**: Add custom entries to the top menu -- **Context Menus**: Create custom context menus for the graph -- **Settings**: Add settings to the ComfyUI settings panel -- **Toasts**: Display notification messages -- **Commands**: Create and register custom commands -- **Hotkeys/Keybindings**: Register custom keyboard shortcuts -- **About Panel Badges**: Add badges to the about panel -- **App Events**: Listen to and respond to app events -- **Graph Manipulation**: Programmatically manipulate the workflow graph +- **Sidebar Tabs**: Create custom sidebar panels like this template demonstrates +- **Bottom Bar Panels**: Add panels to the bottom of the UI +- **Top Menu Items**: Add custom entries to the top menu +- **Context Menus**: Create custom context menus for the graph +- **Settings**: Add settings to the ComfyUI settings panel +- **Toasts**: Display notification messages +- **Commands**: Create and register custom commands +- **Hotkeys/Keybindings**: Register custom keyboard shortcuts +- **About Panel Badges**: Add badges to the about panel +- **App Events**: Listen to and respond to app events +- **Graph Manipulation**: Programmatically manipulate the workflow graph For comprehensive documentation on all available APIs, see the [ComfyUI JavaScript Developer Documentation](https://docs.comfy.org/custom-nodes/js/javascript_overview). ### File Structure ``` -ComfyUI-SwissKnifeUtils/ +ComfyUI-SwissArmyKnife/ ├── .github/ # GitHub configurations │ └── workflows/ │ └── react-build.yml # Automatic build and publishing workflow @@ -247,27 +281,30 @@ npm install -D @comfyorg/comfyui-frontend-types ### Steps to Publish 1. Install the comfy-cli tool: - ```bash - pip install comfy-cli - ``` + + ```bash + pip install comfy-cli + ``` 2. Verify your pyproject.toml has the correct metadata: - ```toml - [project] - name = "your_extension_name" # Use a unique name for your extension - description = "Your extension description here." - version = "0.1.0" # Increment this with each update - [tool.comfy] - PublisherId = "your_publisher_id" # Your Registry publisher ID - DisplayName = "Your Extension Display Name" - includes = ["dist/"] # Include built React code (normally ignored by .gitignore) - ``` + ```toml + [project] + name = "your_extension_name" # Use a unique name for your extension + description = "Your extension description here." + version = "0.1.0" # Increment this with each update + + [tool.comfy] + PublisherId = "your_publisher_id" # Your Registry publisher ID + DisplayName = "Your Extension Display Name" + includes = ["dist/"] # Include built React code (normally ignored by .gitignore) + ``` 3. Publish your extension: - ```bash - comfy-cli publish - ``` + + ```bash + comfy-cli publish + ``` 4. When prompted, enter your API key @@ -281,9 +318,11 @@ This template includes a GitHub Actions workflow that automatically publishes to 4. The GitHub Action will automatically run and publish your extension The workflow configuration is set up in `.github/workflows/react-build.yml` and will trigger when: -- The `pyproject.toml` file is modified and pushed to the `main` branch + +- The `pyproject.toml` file is modified and pushed to the `main` branch The workflow automatically: + 1. Sets up Node.js environment 2. Installs dependencies (`npm install`) 3. Builds the React extension (`npm run build`) @@ -303,21 +342,21 @@ npm run test:watch Example tests can be found in the `src/__tests__` directory. The setup includes: -- Jest for running tests -- React Testing Library for testing components -- Mock implementation of the ComfyUI window.app object +- Jest for running tests +- React Testing Library for testing components +- Mock implementation of the ComfyUI window.app object ## Resources -- [ComfyUI JS Extension Documentation](https://docs.comfy.org/custom-nodes/js/javascript_overview) - Official documentation for ComfyUI JavaScript Extensions -- [ComfyUI Registry Documentation](https://docs.comfy.org/registry/publishing) - Learn how to publish your extension -- [ComfyUI Frontend Repository](https://github.com/sammykumar/ComfyUI-Frontend) - The main ComfyUI frontend codebase -- [Official ComfyUI Frontend Types](https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types) - TypeScript definitions for ComfyUI -- [React Extension Guide](REACT_EXTENSION_GUIDE.md) - Detailed guide for creating React extensions -- [TypeScript Documentation](https://www.typescriptlang.org/docs/) -- [React Documentation](https://react.dev/reference/react) -- [Jest Documentation](https://jestjs.io/docs/getting-started) -- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) +- [ComfyUI JS Extension Documentation](https://docs.comfy.org/custom-nodes/js/javascript_overview) - Official documentation for ComfyUI JavaScript Extensions +- [ComfyUI Registry Documentation](https://docs.comfy.org/registry/publishing) - Learn how to publish your extension +- [ComfyUI Frontend Repository](https://github.com/sammykumar/ComfyUI-Frontend) - The main ComfyUI frontend codebase +- [Official ComfyUI Frontend Types](https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types) - TypeScript definitions for ComfyUI +- [React Extension Guide](REACT_EXTENSION_GUIDE.md) - Detailed guide for creating React extensions +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [React Documentation](https://react.dev/reference/react) +- [Jest Documentation](https://jestjs.io/docs/getting-started) +- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) ## Contributing @@ -325,4 +364,4 @@ Contributions are welcome! Feel free to open issues or submit pull requests to i ## License -GNU General Public License v3 \ No newline at end of file +GNU General Public License v3 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9af04bd --- /dev/null +++ b/__init__.py @@ -0,0 +1,12 @@ +from .utils.nodes import NODE_CLASS_MAPPINGS as MAIN_NODE_CLASS_MAPPINGS +from .utils.nodes import NODE_DISPLAY_NAME_MAPPINGS as MAIN_NODE_DISPLAY_NAME_MAPPINGS +from .utils.helper_nodes import HELPER_NODE_CLASS_MAPPINGS +from .utils.helper_nodes import HELPER_NODE_DISPLAY_NAME_MAPPINGS + +# Combine main nodes and helper nodes +NODE_CLASS_MAPPINGS = {**MAIN_NODE_CLASS_MAPPINGS, **HELPER_NODE_CLASS_MAPPINGS} +NODE_DISPLAY_NAME_MAPPINGS = {**MAIN_NODE_DISPLAY_NAME_MAPPINGS, **HELPER_NODE_DISPLAY_NAME_MAPPINGS} + +WEB_DIRECTORY = "./web" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/docs/CACHING.md b/docs/CACHING.md new file mode 100644 index 0000000..3f764b8 --- /dev/null +++ b/docs/CACHING.md @@ -0,0 +1,162 @@ +# Gemini Media Description Caching + +This document explains the caching functionality implemented for the Gemini custom nodes. + +## Overview + +The caching system automatically stores Gemini API responses to avoid redundant API calls when analyzing the same media with the same description prompt. This provides: + +- **Fast repeated analyses** - Cached results return in milliseconds instead of waiting for API calls +- **API cost savings** - Avoids duplicate API requests for identical media+prompt combinations +- **Transparent operation** - Caching is always enabled and requires no user configuration +- **Accurate isolation** - Separate cache entries for each unique media+prompt+model combination + +## How It Works + +### Cache Key Generation + +Each cache entry uses a unique key based on: + +1. **Media identifier** - Uniquely identifies the media content +2. **Description mode** - The selected prompt option (e.g., "Describe without clothing") +3. **Model settings** - The Gemini model name and type + +### Media Identification + +- **Video files**: Uses file path + modification time + file size +- **Image tensors**: Uses content hash of the tensor data +- **Uploaded files**: Uses the file path in ComfyUI's input directory + metadata + +### Description Modes + +Caching works with all 4 description modes: + +- "Describe without clothing" +- "Describe with clothing" +- "Describe without clothing (No bokeh)" +- "Describe with clothing (No bokeh)" + +Each mode creates a separate cache entry for the same media file. + +### Storage + +Cache entries are stored as JSON files in the `cache/gemini_descriptions/` directory: + +- Each file contains the description, metadata, and timestamp +- Cache persists across ComfyUI restarts +- Cache directory is created automatically +- Cache files are excluded from git via `.gitignore` + +## Cache Behavior + +### Cache Hit (Fast Path) + +``` +User analyzes media → Check cache → Found → Return cached description (< 1ms) +``` + +### Cache Miss (API Path) + +``` +User analyzes media → Check cache → Not found → Call Gemini API → Store result → Return description +``` + +### Cache Isolation Examples + +Same video file with different prompts: + +- `video.mp4` + "Describe without clothing" → Cache entry A +- `video.mp4` + "Describe with clothing" → Cache entry B +- `video.mp4` + "Describe without clothing (No bokeh)" → Cache entry C +- `video.mp4` + "Describe with clothing (No bokeh)" → Cache entry D + +Same prompt with different models: + +- `video.mp4` + "Describe without clothing" + `gemini-2.5-flash` → Cache entry A +- `video.mp4` + "Describe without clothing" + `gemini-2.5-pro` → Cache entry B + +## Implementation Details + +### Files Modified + +- `utils/cache.py` - New cache utility module +- `utils/nodes.py` - Added caching to GeminiVideoDescribe and GeminiImageDescribe classes + +### Cache Integration Points + +1. **Before Gemini API call** - Check cache for existing result +2. **After successful API response** - Store result in cache +3. **Cache hit handling** - Return cached result with appropriate status message + +### Status Messages + +Cache hits are indicated in the Gemini status output: + +``` +🤖 Gemini Analysis Status: ✅ Complete (Cached) +• Model: models/gemini-2.5-flash +• Cached: 2025-09-02 10:16:32 +``` + +Regular API calls show: + +``` +🤖 Gemini Analysis Status: ✅ Complete +• Model: models/gemini-2.5-flash +• API Key: ****1234 +``` + +## Performance Benefits + +- **First analysis**: Normal API call timing (~2-5 seconds) +- **Subsequent analyses**: Cache retrieval (~0.1ms) +- **API savings**: 100% reduction in duplicate calls +- **User experience**: Instant results for repeated workflows + +## Cache Management + +### Automatic Behavior + +- Cache entries are created automatically +- No user configuration required +- Cache persists across sessions +- Old entries remain until manually cleared + +### Manual Management + +The cache can be inspected or cleared programmatically: + +```python +from utils.cache import get_cache + +cache = get_cache() + +# Get cache statistics +info = cache.get_cache_info() +print(f"Entries: {info['entries']}, Size: {info['total_size_mb']} MB") + +# Clear specific entry (not exposed in UI) +# cache._get_cache_file_path(key) and os.remove() +``` + +### Cache Directory Location + +- Default: `/cache/gemini_descriptions/` +- Contains JSON files with SHA256 hash names +- Safe to delete entire directory to clear all cache + +## User Workflow Impact + +The caching is completely transparent to users: + +1. **First time analyzing media**: Works exactly as before (API call) +2. **Re-analyzing same media+prompt**: Results appear instantly from cache +3. **Different prompts on same media**: Each creates new cache entry +4. **Different media files**: Each gets separate cache entries + +Users will notice: + +- Faster repeated analyses +- Cache hit indicators in status messages +- No additional configuration needed +- Consistent behavior across ComfyUI restarts diff --git a/docs/CLOTHING_TEXT_EXCLUSION.md b/docs/CLOTHING_TEXT_EXCLUSION.md new file mode 100644 index 0000000..72d989f --- /dev/null +++ b/docs/CLOTHING_TEXT_EXCLUSION.md @@ -0,0 +1,77 @@ +# Clothing Text and Typography Exclusion Enhancement + +## Overview + +Enhanced the Gemini clothing description prompts to exclude any text, typography, words, letters, logos, brand names, or written content visible on clothing or accessories. This addresses the issue where brand names like "PSD" on shorts would be included in descriptions. + +## Changes Made + +### Before + +The clothing description prompts only excluded "logos or brand names": + +``` +Do not describe logos or brand names. Exclude tattoos, glasses, and other prohibited attributes. +``` + +### After + +Enhanced exclusion to cover all forms of text and typography: + +``` +Do not describe any text, typography, words, letters, logos, brand names, or written content visible on clothing or accessories. Exclude tattoos, glasses, and other prohibited attributes. +``` + +## Implementation Details + +The changes were applied to both image and video processing in `utils/nodes.py`: + +1. **Image Processing** (Line ~174): Updated the CLOTHING paragraph prompt in the `_process_image` method +2. **Video Processing** (Line ~416): Updated the CLOTHING paragraph prompt in the `_process_video` method + +Both instances now use the comprehensive text exclusion language. + +## Examples of Exclusions + +### What Gets Excluded Now ✅ + +- Brand names: "PSD", "Nike", "Adidas", "Supreme" +- Typography: Stylized fonts, decorative lettering +- Words/Text: Any readable text on clothing +- Letters: Individual letters or monograms +- Written content: Printed messages, slogans, quotes +- Logos: Company logos, symbols with text components + +### What Still Gets Described ✅ + +- Garment type and style +- Colors and materials +- Fit and silhouette +- Construction details (seams, straps, waistbands) +- Fabric texture and behavior +- Overall design elements (excluding text) + +## Benefits + +1. **Cleaner Descriptions**: No unwanted brand references in generated prompts +2. **Copyright Compliance**: Avoids reproducing trademarked brand names +3. **Focus on Style**: Emphasizes actual clothing design over commercial messaging +4. **Consistent Exclusion**: Comprehensive coverage of all text-related elements + +## Technical Notes + +- Changes maintain all existing functionality +- No breaking changes to API or node structure +- Applies to both Text2Image and Video processing modes +- Works with all configurable options (describe_clothing enabled) +- Backward compatible with existing workflows + +## Usage + +This enhancement automatically applies when: + +- Using `GeminiMediaDescribe` node with clothing descriptions enabled +- Processing images or videos with visible text on clothing +- Any workflow where clothing description is part of the output + +No configuration changes needed - the improvement is applied automatically to all clothing descriptions. diff --git a/docs/CONFIGURABLE_OPTIONS_GUIDE.md b/docs/CONFIGURABLE_OPTIONS_GUIDE.md new file mode 100644 index 0000000..c6ebe89 --- /dev/null +++ b/docs/CONFIGURABLE_OPTIONS_GUIDE.md @@ -0,0 +1,126 @@ +# Gemini Util - Configurable Options Guide + +## Overview + +The Gemini Describe functionality has been enhanced with configurable options through two nodes: + +1. **Gemini Util - Options**: Configuration node that defines how descriptions should be generated +2. **Gemini Util - Media Describe**: Processing node that accepts media and optional configuration + +## New Gemini Util - Options Node + +### Purpose +This node provides granular control over description generation by separating configuration from media processing. Connect this node's output to the `gemini_options` input of the Media Describe node. + +### Configuration Options + +#### API & Model Settings +- **Gemini API Key**: Your Google Gemini API key +- **Gemini Model**: Choose from available models (2.5-flash, 2.5-flash-lite, 2.5-pro) +- **Model Type**: Text2Image or ImageEdit workflow type + +#### Description Control (New Boolean Options) +- **Describe Clothing?** [Yes/No]: Include detailed clothing and accessory descriptions +- **Describe Hair Style?** [Yes/No]: Include hair texture and motion (but not color/length) +- **Describe Bokeh?** [Yes/No]: Allow depth of field effects and blur descriptions + +#### Text Options +- **Prefix Text**: Text to prepend to generated descriptions + +## Updated Media Describe Node + +### Changes +- **Removed**: API key, model selection, description mode combo box, prefix text +- **Added**: Optional `gemini_options` input that accepts configuration from Options node +- **Maintained**: All media handling capabilities (upload, random selection, image/video support) + +### Backward Compatibility +When no Options node is connected, the Media Describe node uses these defaults: +- Describe Clothing: No +- Describe Hair Style: Yes +- Describe Bokeh: Yes +- API Key: Default development key +- Model: gemini-2.5-flash + +## Usage Examples + +### Basic Usage +1. Add "Gemini Util - Options" node to workflow +2. Configure desired settings +3. Add "Gemini Util - Media Describe" node +4. Connect Options node output to Media Describe `gemini_options` input +5. Provide media input (image tensor or upload) + +### Option Combinations + +#### For Minimal Descriptions (No Clothing, No Hair, No Bokeh) +- Clean, simple descriptions focusing on core elements +- Results in 3-paragraph structure: Subject, Cinematic, Style + +#### For Detailed Fashion Analysis (Clothing + Hair) +- Comprehensive descriptions including garments and hairstyles +- Results in 4-paragraph structure when clothing enabled + +#### For Sharp Focus Photography (No Bokeh) +- Explicitly prevents depth-of-field language +- Useful for product photography or architectural scenes + +### System Prompt Improvements + +#### Enhanced Decisiveness +All prompts now include instructions to avoid uncertain language: +- ❌ "She appears to be wearing either lace tights or leggings" +- ✅ "She wears black lace tights" + +#### Hair Style Option +New granular control over hair descriptions: +- When enabled: Includes texture and movement +- When disabled: Completely omits hair references +- Always excludes: Color and length (as before) + +#### Dynamic Paragraph Structure +- 3 paragraphs: Subject, Cinematic, Style (no clothing) +- 4 paragraphs: Subject, Cinematic, Style, Clothing (with clothing) +- 5-6 paragraphs for video: Adds Scene and Movement sections + +## Migration Guide + +### For Existing Workflows +Existing Media Describe nodes will continue working with default settings. To use new features: + +1. Add Options node to workflow +2. Configure desired settings +3. Connect to Media Describe node +4. Remove any hardcoded API keys from Media Describe node + +### Option Mapping +Old combo box "Description Mode" maps to new options as follows: + +- "Describe without clothing" → Clothing: No, Hair: Yes, Bokeh: Yes +- "Describe with clothing" → Clothing: Yes, Hair: Yes, Bokeh: Yes +- "Describe without clothing (No bokeh)" → Clothing: No, Hair: Yes, Bokeh: No +- "Describe with clothing (No bokeh)" → Clothing: Yes, Hair: Yes, Bokeh: No + +## Benefits + +1. **Modularity**: Configure once, use with multiple media nodes +2. **Flexibility**: Mix and match options as needed +3. **Clarity**: Each option has a clear purpose +4. **Extensibility**: Easy to add new options in the future +5. **Precision**: More deterministic descriptions without uncertainty + +## Troubleshooting + +### No Description Generated +- Verify Options node is connected to Media Describe +- Check API key is valid +- Ensure media input is provided + +### Unexpected Content +- Review individual option settings +- Check if hair/clothing/bokeh options match expectations +- Verify model type (Text2Image vs ImageEdit) is appropriate + +### Backward Compatibility Issues +- Media Describe works standalone with defaults +- Only connect Options node if you need custom configuration \ No newline at end of file diff --git a/docs/DECISIVENESS_IMPROVEMENTS.md b/docs/DECISIVENESS_IMPROVEMENTS.md new file mode 100644 index 0000000..14ba354 --- /dev/null +++ b/docs/DECISIVENESS_IMPROVEMENTS.md @@ -0,0 +1,83 @@ +# Gemini Prompt Decisiveness Improvements + +## Overview + +Enhanced the Gemini prompts to eliminate uncertainty language and provide more decisive, confident descriptions. The changes address the issue where Gemini would respond with uncertain phrases like "holding a black folder or book" instead of making a definitive choice. + +## Changes Made + +### 1. Text2Image Prompts (Image Processing) + +**Added prominent decisiveness requirement early in system prompt:** + +``` +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". +``` + +**Enhanced critical note to be more explicit:** + +- Added comprehensive list of uncertainty phrases to avoid +- Added instruction to "confidently choose one and state it as absolute fact" + +### 2. ImageEdit Prompts + +**Added decisiveness requirement to single-sentence ImageEdit instructions:** + +``` +Always be completely decisive and definitive - when you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". +``` + +### 3. Video Processing Prompts + +**Added same decisiveness requirement to video analysis:** + +- Includes the same prominent decisiveness section as Text2Image +- Enhanced critical note with comprehensive uncertainty language elimination +- Improved clothing descriptions to be more definitive + +### 4. Clothing Descriptions + +**Enhanced clothing description sections for both image and video:** + +- Added "with confidence" and "definitiveness" language +- Included instruction to "Make decisive choices when multiple interpretations are possible" +- Emphasized choosing "one specific description and state it as fact" + +## Examples of Improvements + +### Before (Uncertain Language) + +- ❌ "She appears to be wearing either lace tights or leggings" +- ❌ "holding what might be a black folder or book" +- ❌ "seems to be wearing denim, possibly jeans" +- ❌ "likely has curly hair texture" + +### After (Decisive Language) + +- ✅ "She wears black lace tights" +- ✅ "holding a black folder" +- ✅ "wearing dark blue denim jeans" +- ✅ "has curly hair texture" + +## Benefits + +1. **Eliminates Ambiguity**: No more "either/or" constructions +2. **Improves Prompt Quality**: More specific descriptions for better AI generation +3. **Reduces Uncertainty**: Confident descriptions lead to better results +4. **Better User Experience**: Clear, definitive outputs instead of wishy-washy descriptions + +## Technical Implementation + +- Changes applied to `utils/nodes.py` in the `GeminiMediaDescribe` class +- Affects all three processing modes: Text2Image, ImageEdit, and Video +- Maintains backward compatibility with existing options +- No breaking changes to API or node structure + +## Validation + +- ✅ Python syntax validation passed +- ✅ Node class mappings intact +- ✅ All functionality preserved +- ✅ Enhanced decisiveness implemented across all prompt types + +The prompts now consistently guide Gemini to make confident, definitive choices rather than expressing uncertainty through hedging language. diff --git a/docs/FINAL_STRING_WIDGET_FIX.md b/docs/FINAL_STRING_WIDGET_FIX.md new file mode 100644 index 0000000..030ce9d --- /dev/null +++ b/docs/FINAL_STRING_WIDGET_FIX.md @@ -0,0 +1,86 @@ +# ComfyUI-SwissArmyKnife - Final String Widget Demo + +## Issue Fixed + +The `final_string` widget was not being populated when the ComfyUI nodes executed. This was because: + +1. ✅ Python nodes correctly returned `final_string` as output +2. ✅ JavaScript widgets created the `finalStringWidget` +3. ❌ **Missing**: No mechanism to update the widget with execution results + +## Solution Applied + +Added `onExecuted` methods to both node types that: + +1. Listen for node execution completion +2. Extract the `final_string` output from the execution results +3. Update the `finalStringWidget.value` with the actual generated text + +## How to Test + +### 1. Using GeminiUtilImageDescribe Node + +1. Add a "Gemini Util - Image Describe" node to your workflow +2. Connect an IMAGE input (from Load Image or another source) +3. Configure your Gemini API key and prompts +4. Execute the workflow (Queue Prompt) +5. **Check**: The `final_string` widget should populate with the actual generated description + +### 2. Using GeminiUtilVideoDescribe Node + +1. Add a "Gemini Util - Video Describe" node to your workflow +2. Upload a video using the "📁 Choose Video to Upload" button +3. Configure your Gemini API key and prompts +4. Execute the workflow (Queue Prompt) +5. **Check**: The `final_string` widget should populate with the actual generated description + +### 3. Using the ShowText Helper Node + +1. Add a "Show Text" node to your workflow +2. Connect the `final_string` output from either Gemini node to the `text` input of ShowText +3. Execute the workflow +4. **Result**: ShowText node will display the final generated prompt text + +## Example Workflow + +``` +[Load Image] → [GeminiUtilImageDescribe] → [ShowText] + ↓ + final_string widget updates + ↓ + [Connect final_string output to ShowText input] +``` + +## Technical Details + +### JavaScript Changes (web/js/gemini_widgets.js) + +- Added `onExecuted` method for `GeminiUtilVideoDescribe` (output index 4) +- Added `onExecuted` method for `GeminiUtilImageDescribe` (output index 2) +- Widget updates are logged to browser console for debugging + +### Python Changes + +- Added `ShowText` helper node in `utils/helper_nodes.py` +- Updated `__init__.py` to include helper nodes +- All nodes return `final_string` as documented + +### Before (Broken) + +``` +final_string widget: "Populated Prompt (Will be generated automatically)" +``` + +### After (Fixed) + +``` +final_string widget: "A woman with flowing hair stands gracefully in a sunlit garden. The scene unfolds on a wooden deck overlooking rolling hills..." +``` + +## Debugging + +- Check browser console for "Updated final_string widget with:" messages +- Verify execution completes without errors +- Ensure API key is valid and has quota + +The `final_string` widget should now properly display the generated prompt text after node execution! diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md new file mode 100644 index 0000000..6fc6fbb --- /dev/null +++ b/docs/IMPLEMENTATION_STATUS.md @@ -0,0 +1,85 @@ +# GeminiMediaDescribe Implementation Summary + +## ✅ COMPLETED SUCCESSFULLY + +### 1. Python Backend Implementation + +- **File**: `utils/nodes.py` +- **Status**: ✅ Working correctly +- **Features**: + - Clean GeminiMediaDescribe class with proper INPUT_TYPES structure + - Support for media_source: "Upload Media" vs "Randomize Media from Path" + - Support for media_type: "image" vs "video" + - Proper error handling for missing inputs + - File randomization logic using glob patterns + - All existing GeminiVideoDescribe and GeminiImageDescribe classes preserved + +### 2. JavaScript Frontend Implementation + +- **File**: `web/js/gemini_widgets.js` +- **Status**: ✅ Working correctly +- **Features**: + - Dynamic UI widgets that show/hide based on media_source selection + - Upload widgets appear when "Upload Media" is selected + - Path input appears when "Randomize Media from Path" is selected + - Proper widget management and event handling + +### 3. Input Parameters Structure + +```javascript +Required: +- gemini_api_key (STRING) +- gemini_model (["models/gemini-2.5-flash", "models/gemini-2.5-flash-lite", "models/gemini-2.5-pro"]) +- model_type (["Text2Image", "ImageEdit"]) +- description_mode (["Describe without clothing", "Describe with clothing", ...]) +- prefix_text (STRING, multiline) +- media_source (["Upload Media", "Randomize Media from Path"]) ⭐ NEW +- media_type (["image", "video"]) ⭐ NEW + +Optional: +- image (IMAGE tensor) +- media_path (STRING) ⭐ NEW +- uploaded_image_file (STRING) ⭐ NEW +- uploaded_video_file (STRING) ⭐ NEW +``` + +### 4. Functionality Tests + +- ✅ Python imports work correctly +- ✅ Node class definitions are valid +- ✅ INPUT_TYPES structure is properly formatted +- ✅ Error handling works for missing inputs +- ✅ Function calls execute without syntax errors +- ✅ Package initialization still works + +### 5. Return Values + +The node returns 3 outputs: + +1. **description**: The generated description or error message +2. **gemini_status**: Detailed status information with emoji indicators +3. **final_string**: Concatenated prefix_text + description + +## 🎯 CURRENT STATUS: READY FOR TESTING + +The consolidated GeminiMediaDescribe node is now fully implemented and ready for UI testing in ComfyUI. The implementation supports: + +1. **Upload Media Mode**: Users can upload images/videos via widgets +2. **Randomize Media from Path Mode**: Users can specify a directory path and the node will randomly select a file +3. **Dynamic UI**: The interface adapts based on user selections +4. **Proper Error Handling**: Clear error messages for missing inputs or invalid paths +5. **Testing Framework**: Basic validation returns test responses + +## 🔜 NEXT STEPS + +1. Test the node in ComfyUI interface to verify widget behavior +2. Implement actual Gemini API processing (currently returns test responses) +3. Add file validation and additional media format support +4. Test random file selection with actual media directories + +## 📁 Files Modified + +- `utils/nodes.py` - Clean implementation with all three node classes +- `web/js/gemini_widgets.js` - Dynamic UI widgets (already working) +- `utils/nodes_corrupted_backup.py` - Backup of broken version +- `utils/nodes_clean.py` - Clean working version (used to restore nodes.py) diff --git a/docs/JAVASCRIPT_ERROR_FIX.md b/docs/JAVASCRIPT_ERROR_FIX.md new file mode 100644 index 0000000..09c8d27 --- /dev/null +++ b/docs/JAVASCRIPT_ERROR_FIX.md @@ -0,0 +1,60 @@ +## JavaScript Module Error Resolution + +### Problem + +ComfyUI was showing JavaScript errors when trying to load modules from non-existent paths: + +``` +Error loading extension /extensions/comfyui_swissarmyknife/node_modules/ajv/lib/dotjs/allOf.js +Error loading extension /extensions/comfyui_swissarmyknife/node_modules/ajv/lib/dotjs/multipleOf.js +Error loading extension /extensions/comfyui_swissarmyknife/node_modules/ajv/lib/dotjs/dependencies.js +Error loading extension /extensions/comfyui_swissarmyknife/node_modules/ajv/lib/dotjs/required.js +Error loading extension /extensions/comfyui_swissarmyknife/node_modules/ajv/lib/dotjs/validate.js +Error loading extension /extensions/comfyui_swissarmyknife/node_modules/ajv/lib/dotjs/format.js +Error loading extension /extensions/comfyui_swissarmyknife/node_modules/ajv/lib/dotjs/enum.js +``` + +### Root Cause + +The `web/node_modules` directory contained development dependencies (ESLint, Prettier, etc.) that were accidentally being served by ComfyUI as part of the web extension. ComfyUI was attempting to load these Node.js modules as browser JavaScript files, which is incorrect. + +### Solution Applied + +1. **Removed `web/node_modules`** - This directory contained development dependencies that shouldn't be exposed to ComfyUI +2. **Removed `web/package-lock.json`** - No longer needed since we're not installing dependencies in the web directory +3. **Kept `web/package.json`** - This is fine for development tool configuration (ESLint, Prettier) + +### Architecture Clarification + +According to the project structure: + +- **`web/js/gemini_widgets.js`** - Plain JavaScript widgets for ComfyUI (✅ Active) +- **`web/css/gemini_widgets.css`** - CSS for widgets (✅ Active) +- **`ui-react_backup/`** - React UI extension (⚠️ Disabled) +- **Node modules should only exist in**: + - Root directory (`node_modules/`) for Playwright tests + - React UI directory (`ui-react_backup/ui/node_modules/`) for React build + - Test directory (`web/tests/node_modules/`) for Playwright web tests + +### Expected Result + +After removing the problematic `web/node_modules` directory, ComfyUI should no longer attempt to load the ajv modules that were causing the JavaScript errors. The plain JavaScript widgets will continue to work normally. + +### Files Affected + +- ✅ Removed: `/web/node_modules/` (entire directory) +- ✅ Removed: `/web/package-lock.json` +- ✅ Kept: `/web/package.json` (for development tools only) +- ✅ Kept: `/web/js/gemini_widgets.js` (main widget functionality) +- ✅ Kept: `/web/css/gemini_widgets.css` (widget styling) + +### Verification + +To verify the fix is working: + +1. Restart ComfyUI server +2. Clear browser cache +3. Check browser console for JavaScript errors +4. Confirm that Gemini nodes load and function correctly + +The JavaScript errors should now be resolved since ComfyUI will no longer try to serve the development dependencies as web assets. diff --git a/docs/SEED_WIDGET_IMPLEMENTATION.md b/docs/SEED_WIDGET_IMPLEMENTATION.md new file mode 100644 index 0000000..dbada41 --- /dev/null +++ b/docs/SEED_WIDGET_IMPLEMENTATION.md @@ -0,0 +1,151 @@ +# Seed Widget Implementation - Documentation + +## Overview + +This document describes the completed implementation of the seed widget functionality for the GeminiMediaDescribe node, which was restored after being accidentally removed in a previous commit. + +## Problem Solved + +When using "Randomize Media from Path" mode, ComfyUI's execution engine would skip re-running the node if the `media_path` input stayed the same, because ComfyUI only re-executes nodes when their inputs change. This is standard ComfyUI behavior. + +## Solution Implemented + +Added a `seed` parameter that: + +1. **Forces Re-execution**: When the seed value changes, ComfyUI recognizes the node inputs have changed and re-executes +2. **Smart Visibility**: Only appears when "Randomize Media from Path" is selected +3. **Easy Randomization**: Includes a "🎲 Randomize Seed" button for quick seed generation +4. **Reproducible Results**: Same seed produces same file selection for reproducible workflows + +## Technical Implementation + +### Python Backend Changes (`utils/nodes.py`) + +1. **Added seed parameter to INPUT_TYPES**: +```python +"seed": ("INT", { + "default": 0, + "min": 0, + "max": 0xFFFFFFFFFFFFFFFF, + "tooltip": "Seed for randomization when using 'Randomize Media from Path'. Use different seeds to force re-execution." +}), +``` + +2. **Updated function signature**: +```python +def describe_media(self, gemini_api_key, gemini_model, model_type, description_mode, prefix_text, media_source, media_type, seed, image=None, ...): +``` + +3. **Implemented seed-based file selection**: +```python +# Randomly select a file using the seed for reproducible selection +# When seed changes, a different file may be selected, forcing re-execution +random.seed(seed) +selected_media_path = random.choice(all_files) + +# Reset random state to avoid affecting other operations +random.seed(None) +``` + +### JavaScript Frontend Changes (`web/js/gemini_widgets.js`) + +1. **Conditional Widget Visibility**: + - Seed widget shows only when `media_source = "Randomize Media from Path"` + - Seed widget is hidden when `media_source = "Upload Media"` + +2. **Randomize Seed Button**: +```javascript +// Add randomize seed button for convenience +this.randomizeSeedWidget = this.addWidget( + "button", + "🎲 Randomize Seed", + "randomize_seed", + () => { + this.onRandomizeSeedButtonPressed(); + } +); +``` + +3. **Random Seed Generation**: +```javascript +nodeType.prototype.onRandomizeSeedButtonPressed = function () { + const seedWidget = this.widgets.find((w) => w.name === "seed"); + if (seedWidget) { + // Generate a random seed (large integer) + const randomSeed = Math.floor(Math.random() * 0xFFFFFFFFFFFFFFFF); + seedWidget.value = randomSeed; + console.log(`[SEED] Generated random seed: ${randomSeed}`); + + // Trigger widget update to ensure ComfyUI recognizes the change + if (seedWidget.callback) { + seedWidget.callback(randomSeed); + } + } +}; +``` + +## User Interface Behavior + +### Upload Media Mode +- Seed widget: **Hidden** +- Randomize seed button: **Hidden** +- Upload widgets: **Visible** + +### Randomize Media from Path Mode +- Seed widget: **Visible** (number input, 0 to 18446744073709551615) +- Randomize seed button: **Visible** (🎲 Randomize Seed) +- Media path widget: **Visible** +- Upload widgets: **Hidden** + +## Usage Instructions + +### For "Randomize Media from Path" Mode: + +1. **Set media_source** to "Randomize Media from Path" +2. **Specify media_path** to your directory containing images/videos +3. **Use the seed widget** to control randomization: + - **Manual**: Enter any number (0 to 18446744073709551615) + - **Automatic**: Click "🎲 Randomize Seed" for instant random generation +4. **Change the seed** to force ComfyUI to re-select a different random file + +### For "Upload Media" Mode: + +- Seed widget is automatically hidden (not relevant for uploaded files) + +## Why This Works + +ComfyUI's execution engine compares all input values to determine if a node needs re-execution: + +- **Before**: Only `media_path` was checked → same path = no re-execution +- **After**: Both `media_path` AND `seed` are checked → different seed = re-execution + +This follows the same pattern used by other ComfyUI nodes like samplers (KSampler, etc.) that include seed parameters. + +## Technical Benefits + +1. **Standard Pattern**: Uses the same approach as built-in ComfyUI nodes +2. **Performance**: Only shows seed widget when needed (clean UI) +3. **User-Friendly**: One-click randomization with the dice button +4. **Persistent**: Seed value is properly saved/restored with workflows +5. **Reproducible**: Same seed always selects the same file from a directory + +## Testing Verification + +All functionality has been verified through: + +1. **Unit Tests**: Python parameter validation and function signature +2. **Integration Tests**: Full node execution with seed parameter +3. **UI Tests**: Widget visibility and button functionality simulation +4. **Code Quality**: Linting with ruff (all checks passed) + +## Console Logging + +The implementation includes debug logging: + +``` +[STATE] Showing seed widget for randomization +[STATE] Hiding seed widget for upload mode +[SEED] Generated random seed: 1234567890123456789 +``` + +This ensures proper debugging and verification of widget behavior in ComfyUI. \ No newline at end of file diff --git a/docs/SEED_WIDGET_RANDOMIZATION_FIX.md b/docs/SEED_WIDGET_RANDOMIZATION_FIX.md new file mode 100644 index 0000000..9a12269 --- /dev/null +++ b/docs/SEED_WIDGET_RANDOMIZATION_FIX.md @@ -0,0 +1,81 @@ +# Seed Widget for Randomization Fix + +## Problem Solved + +When using "Randomize Media from Path", ComfyUI's execution engine would skip re-running the node if the `media_path` input stayed the same, because ComfyUI only re-executes nodes when their inputs change. This is standard ComfyUI behavior. + +## Solution: Seed Widget + +Added a `seed` parameter that: + +1. **Forces Re-execution**: When the seed value changes, ComfyUI recognizes the node inputs have changed and re-executes +2. **Smart Visibility**: Only appears when "Randomize Media from Path" is selected +3. **Easy Randomization**: Includes a "🎲 Randomize Seed" button for quick seed generation + +## Implementation Details + +### Python Node Changes + +```python +# Added to INPUT_TYPES in required section: +"seed": ("INT", { + "default": 0, + "min": 0, + "max": 0xFFFFFFFFFFFFFFFF, + "tooltip": "Seed for randomization when using 'Randomize Media from Path'. Use different seeds to force re-execution." +}), + +# Updated function signature: +def describe_media(self, ..., seed, ...): +``` + +### JavaScript Widget Management + +1. **Conditional Visibility**: Seed widget is hidden by default and only shown when `media_source` is "Randomize Media from Path" + +2. **Randomize Button**: Adds a convenient "🎲 Randomize Seed" button that generates random values + +3. **State Management**: Properly managed in the `updateMediaWidgets()` function + +## Usage Instructions + +### For "Randomize Media from Path" Mode: + +1. **Set media_source** to "Randomize Media from Path" +2. **Specify media_path** to your directory containing images/videos +3. **Use the seed widget** to control randomization: + - **Manual**: Enter any number (0 to 18446744073709551615) + - **Automatic**: Click "🎲 Randomize Seed" for instant random generation +4. **Change the seed** to force ComfyUI to re-select a different random file + +### For "Upload Media" Mode: + +- Seed widget is automatically hidden (not relevant for uploaded files) + +## Why This Works + +ComfyUI's execution engine compares all input values to determine if a node needs re-execution: + +- **Before**: Only `media_path` was checked → same path = no re-execution +- **After**: Both `media_path` AND `seed` are checked → different seed = re-execution + +This follows the same pattern used by other ComfyUI nodes like samplers (KSampler, etc.) that include seed parameters. + +## Technical Benefits + +1. **Standard Pattern**: Uses the same approach as built-in ComfyUI nodes +2. **Performance**: Only shows seed widget when needed (clean UI) +3. **User-Friendly**: One-click randomization with the dice button +4. **Persistent**: Seed value is properly saved/restored with workflows + +## Console Logging + +The implementation includes debug logging: + +``` +[STATE] Showing seed widget for randomization +[STATE] Hiding seed widget for upload mode +[SEED] Generated random seed: 1234567890123456789 +``` + +This ensures ComfyUI will properly re-execute your randomization workflow whenever you want to select a different random file from your directory. diff --git a/docs/WIDGET_INVESTIGATION_AND_FIXES.md b/docs/WIDGET_INVESTIGATION_AND_FIXES.md new file mode 100644 index 0000000..4a1ad77 --- /dev/null +++ b/docs/WIDGET_INVESTIGATION_AND_FIXES.md @@ -0,0 +1,128 @@ +# Widget Investigation and Fixes + +## Problem Analysis + +### Issues Found: + +1. **Visible unwanted widgets**: On load, the GeminiMediaDescribe node was showing `media_path`, `uploaded_image_file`, and `uploaded_video_file` input fields that should have been hidden. + +2. **Video widgets shown when image selected**: Even when `media_type` was set to "image" (default), video-related widgets were still visible. + +3. **Inconsistent node registration**: The JavaScript widget registration used `nodeType.comfyClass == "GeminiUtilMediaDescribe"` instead of the consistent pattern `nodeData.name === "GeminiUtilMediaDescribe"`. + +### Root Causes: + +1. **Missing widget hiding mechanism**: The optional input fields defined in the Python `INPUT_TYPES` were not being hidden by the JavaScript widget system. + +2. **Widget duplication**: The upload functions were creating new hidden widgets instead of using the existing optional input widgets. + +3. **Incomplete widget management**: The dynamic widget system wasn't properly controlling the visibility of the original input widgets. + +## Solutions Implemented: + +### 1. Fixed Node Registration Condition + +**File**: `web/js/gemini_widgets.js` +**Change**: + +```javascript +// BEFORE: +} else if (nodeType.comfyClass == "GeminiUtilMediaDescribe") { + +// AFTER: +} else if (nodeData.name === "GeminiUtilMediaDescribe") { +``` + +### 2. Added Widget Hiding Mechanism + +**Added function** to hide optional input widgets immediately on node creation: + +```javascript +this.hideOptionalInputWidgets = function () { + const widgetsToHide = [ + "media_path", + "uploaded_image_file", + "uploaded_video_file", + ]; + + for (const widgetName of widgetsToHide) { + const widget = this.widgets.find((w) => w.name === widgetName); + if (widget) { + widget.type = "hidden"; + widget.computeSize = () => [0, -4]; // Make it take no space + } + } +}; +``` + +### 3. Improved Widget Management System + +**Updated** `updateMediaWidgets` function to: + +- Find and control the original input widgets instead of creating duplicates +- Show/hide `media_path` widget based on `media_source` selection +- Properly hide unused upload widgets based on `media_type` selection + +### 4. Fixed Upload Functions + +**Updated** both image and video upload functions to: + +- Use the original `uploaded_image_file` and `uploaded_video_file` widgets +- Fall back to creating hidden widgets only if originals don't exist +- Properly update the original widgets with file paths + +### 5. Enhanced State Management + +**Updated** `clearAllMediaState` function to: + +- Clear both custom widgets and original input widgets +- Properly reset all widget values when switching modes + +### 6. Added Missing Features + +**Added** missing `final_string` widget and `onExecuted` handler for the GeminiMediaDescribe node to match the functionality of other nodes. + +## Expected Behavior After Fixes: + +### On Node Load: + +- Only `media_source` and `media_type` dropdowns should be visible +- `media_path`, `uploaded_image_file`, and `uploaded_video_file` should be hidden +- Default configuration: "Upload Media" + "image" should show image upload widgets only + +### When "Upload Media" + "image" is selected: + +- Image upload button and image info widget should be visible +- Video widgets should be hidden +- `media_path` widget should be hidden + +### When "Upload Media" + "video" is selected: + +- Video upload button and video info widget should be visible +- Image widgets should be hidden +- `media_path` widget should be hidden + +### When "Randomize Media from Path" is selected: + +- `media_path` text input should become visible +- All upload widgets should be hidden +- Upload file widgets should remain hidden + +### During File Upload: + +- Uploaded file paths should be stored in the original input widgets +- These widgets remain hidden but contain the correct data for the Python node + +## Testing Recommendations: + +1. **Load the GeminiMediaDescribe node** and verify only expected widgets are visible +2. **Switch between media_source options** and verify appropriate widgets show/hide +3. **Switch between media_type options** and verify appropriate widgets show/hide +4. **Upload files** and verify they're processed correctly +5. **Execute the node** and verify the final_string widget updates properly + +## Files Modified: + +- `web/js/gemini_widgets.js` - Main widget management fixes + +The fixes ensure a clean, intuitive user interface that only shows relevant widgets based on user selections, eliminating confusion and improving usability. diff --git a/docs/WIDGET_STATE_PERSISTENCE_FIX.md b/docs/WIDGET_STATE_PERSISTENCE_FIX.md new file mode 100644 index 0000000..c641ed6 --- /dev/null +++ b/docs/WIDGET_STATE_PERSISTENCE_FIX.md @@ -0,0 +1,110 @@ +# Widget State Persistence Fix + +## Problem Description + +When using the `GeminiUtilMediaDescribe` node: + +1. Change `media_source` to "Randomize Media from Path" +2. Save the workflow +3. Refresh the browser or reload the workflow +4. **Issue**: The "Choose Image to Upload" widget becomes visible again, even though `media_source` is still set to "Randomize Media from Path" + +This happens because ComfyUI doesn't automatically persist JavaScript-controlled widget visibility changes when saving/loading workflows. + +## Solution Implemented + +Added ComfyUI's standard serialization system using `onSerialize` and `onConfigure` methods: + +### 1. State Serialization (`onSerialize`) + +- Saves current UI state (media_source and media_type values) when workflow is saved +- State is stored in the workflow JSON file + +### 2. State Restoration (`onConfigure`) + +- Restores UI state when workflow is loaded +- Sets widget values and calls `updateMediaWidgets()` to update visibility + +### 3. Workflow Loading Hook (`loadedGraphNode`) + +- Ensures UI state is applied when workflows are loaded from files +- Provides fallback for edge cases + +## Technical Implementation + +```javascript +// Save UI state during workflow save +const onSerialize = nodeType.prototype.onSerialize; +nodeType.prototype.onSerialize = function (o) { + const result = onSerialize?.apply(this, arguments); + + o.ui_state = { + media_source: this.mediaSourceWidget?.value || "Upload Media", + media_type: this.mediaTypeWidget?.value || "image", + }; + + return result; +}; + +// Restore UI state during workflow load +const onConfigure = nodeType.prototype.onConfigure; +nodeType.prototype.onConfigure = function (o) { + const result = onConfigure?.apply(this, arguments); + + if (o.ui_state) { + // Set widget values + if (this.mediaSourceWidget && o.ui_state.media_source) { + this.mediaSourceWidget.value = o.ui_state.media_source; + } + if (this.mediaTypeWidget && o.ui_state.media_type) { + this.mediaTypeWidget.value = o.ui_state.media_type; + } + + // Update UI to match restored state + setTimeout(() => { + this.updateMediaWidgets(); + }, 0); + } + + return result; +}; +``` + +## Testing Steps + +1. **Set up the node**: + + - Add a `GeminiUtilMediaDescribe` node to your workflow + - Change `media_source` to "Randomize Media from Path" + - Verify that the image upload widgets are hidden + +2. **Save and reload**: + + - Save the workflow (Ctrl+S) + - Refresh the browser or reload ComfyUI + - Load the saved workflow + +3. **Verify fix**: + - ✅ The `media_source` should still be "Randomize Media from Path" + - ✅ The image upload widgets should remain hidden + - ✅ Only the `media_path` text input should be visible + +## Benefits + +- **Persistent UI State**: Widget visibility is now properly saved/restored +- **Consistent UX**: Users don't need to reconfigure the UI after reloading +- **Standard Approach**: Uses ComfyUI's official serialization system +- **Compatible**: Works with workflow sharing and version control + +## Console Logging + +The fix includes debug logging to help track state persistence: + +``` +[SERIALIZE] Saving UI state: {media_source: "Randomize Media from Path", media_type: "image"} +[CONFIGURE] Restoring UI state: {media_source: "Randomize Media from Path", media_type: "image"} +[CONFIGURE] UI state restored and widgets updated +[LOADED] Applied UI state for loaded workflow node +``` + +This fix follows ComfyUI best practices and is the standard solution used by other custom nodes with dynamic UI elements. diff --git a/docs/WORKFLOW_DEMO.md b/docs/WORKFLOW_DEMO.md new file mode 100644 index 0000000..92c8ad7 --- /dev/null +++ b/docs/WORKFLOW_DEMO.md @@ -0,0 +1,46 @@ +## Final String Widget Demo Workflow + +This shows how to set up a workflow to test the final_string widget functionality: + +``` +┌─────────────┐ ┌─────────────────────────┐ ┌─────────────┐ +│ Load Image │ │ Gemini Image Describe │ │ Show Text │ +│ │────▶│ │────▶│ │ +│ [Load your │ │ - API Key: [your key] │ │ Displays │ +│ test image]│ │ - Model: gemini-2.5- │ │ final │ +│ │ │ flash │ │ generated │ +└─────────────┘ │ - System Prompt: [...] │ │ prompt │ + │ - User Prompt: [...] │ └─────────────┘ + │ │ + │ ┌─────────────────────┐ │ + │ │ final_string widget │ │ ← This widget will + │ │ (will auto-update) │ │ populate after + │ └─────────────────────┘ │ execution! + └─────────────────────────┘ +``` + +### Step-by-step Instructions: + +1. **Import the workflow**: + - Copy the contents of `example_workflow.json` + - In ComfyUI, go to "Load" → "Load (from file)" or drag the JSON file + +2. **Set up the nodes**: + - Load Image: Choose any test image + - Gemini Image Describe: Enter your valid Gemini API key + - Show Text: This will display the final output + +3. **Execute the workflow**: + - Click "Queue Prompt" to run the workflow + - Wait for the Gemini API to process the image + +4. **Check the results**: + - The `final_string` widget on the Gemini node should populate with generated text + - The Show Text node should display the same text + - Browser console should show "Updated final_string widget with:" message + +### What was fixed: +- ❌ Before: final_string widget showed placeholder text +- ✅ After: final_string widget shows actual generated description + +The widget update happens automatically when the node execution completes! diff --git a/docs/example_workflow.json b/docs/example_workflow.json new file mode 100644 index 0000000..cdc0b38 --- /dev/null +++ b/docs/example_workflow.json @@ -0,0 +1,89 @@ +{ + "last_node_id": 4, + "last_link_id": 3, + "nodes": [ + { + "id": 1, + "type": "LoadImage", + "pos": [100, 100], + "size": { "0": 315, "1": 314 }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [1], + "shape": 3, + "slot_index": 0 + }, + { "name": "MASK", "type": "MASK", "links": null, "shape": 3 } + ], + "properties": { "Node name for S&R": "LoadImage" }, + "widgets_values": ["example.jpg", "image"] + }, + { + "id": 2, + "type": "GeminiUtilImageDescribe", + "pos": [500, 100], + "size": { "0": 400, "1": 500 }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [{ "name": "image", "type": "IMAGE", "link": 1 }], + "outputs": [ + { + "name": "description", + "type": "STRING", + "links": null, + "shape": 3 + }, + { + "name": "gemini_status", + "type": "STRING", + "links": null, + "shape": 3 + }, + { + "name": "final_string", + "type": "STRING", + "links": [2], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { "Node name for S&R": "GeminiUtilImageDescribe" }, + "widgets_values": [ + "YOUR_GEMINI_API_KEY_HERE", + "models/gemini-2.5-flash", + "Generate a detailed image description...", + "Please analyze this image...", + "" + ] + }, + { + "id": 3, + "type": "ShowText", + "pos": [1000, 100], + "size": { "0": 300, "1": 200 }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [{ "name": "text", "type": "STRING", "link": 2 }], + "outputs": [ + { "name": "text", "type": "STRING", "links": null, "shape": 3 } + ], + "properties": { "Node name for S&R": "ShowText" }, + "widgets_values": [""] + } + ], + "links": [ + [1, 1, 0, 2, 0, "IMAGE"], + [2, 2, 2, 3, 0, "STRING"] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} diff --git a/docs/gemini-prompts.md b/docs/gemini-prompts.md new file mode 100644 index 0000000..3d2ab06 --- /dev/null +++ b/docs/gemini-prompts.md @@ -0,0 +1,371 @@ +# Gemini Prompts Documentation + +This document outlines the system prompts and user prompts used in the ComfyUI-SwissArmyKnife Gemini integration for ComfyUI. + +## System Prompts + +### Text2Image System Prompt (Images) + +``` +Generate a Wan 2.2 optimized text to image prompt. You are an expert assistant specialized in analyzing and verbalizing input media for instagram-quality posts using the Wan 2.2 Text to Image workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review the provided media. Do not use meta phrases (e.g., "this picture shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +{combined_prompts}{clothing_prompt} + +{critical_note} +``` + +### ImageEdit System Prompt (Images with Subject) + +``` +You are an expert assistant generating concise, single-sentence Qwen-Image-Edit instructions. Always be completely decisive and definitive - when you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". + +Always begin with "Make this person…", include vivid, focused scene details (e.g. bedroom props, lights, furniture or gym bench, textured wall, window views) early to anchor the setting{focus_instruction}, {clothing_note}, include clear torso and head orientation (e.g., "back facing the camera with torso turned 45° and head looking over her shoulder toward viewer"), reference cinematic aesthetic cues (lighting, framing, lens, shot type), anchor realism by stating skin shows subtle pores, light wrinkles, and realistic surface detail, end with "keep everything else unchanged," and include negative safeguards like "no distortion, no blur artifacts{focus_safeguards}." +``` + +### ImageEdit System Prompt (Images without Subject) + +``` +You are an expert assistant generating concise, single-sentence Qwen-Image-Edit instructions. Always be completely decisive and definitive - when you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". + +Focus on vivid, focused scene details (e.g. bedroom props, lights, furniture or gym bench, textured wall, window views) to anchor the setting{focus_instruction}, do not describe people or human subjects, reference cinematic aesthetic cues (lighting, framing, lens, shot type), end with "keep everything else unchanged," and include negative safeguards like "no distortion, no blur artifacts{focus_safeguards}." +``` + +### Video System Prompt + +``` +You are an expert assistant specialized in analyzing and verbalizing input videos for cinematic-quality video transformation using the Wan 2.2 + VACE workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review all provided frames as a single clip and infer motion across time; reason stepwise over the entire sequence (start → middle → end). Do not use meta phrases (e.g., "this video shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +{combined_prompts}{clothing_prompt} + +{critical_note} +``` + +## System & User Prompts Expanded (Full) + +### Text2Image (Images) - All Options Enabled + +**System Prompt:** + +``` +Generate a Wan 2.2 optimized text to image prompt. You are an expert assistant specialized in analyzing and verbalizing input media for instagram-quality posts using the Wan 2.2 Text to Image workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review the provided media. Do not use meta phrases (e.g., "this picture shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +SUBJECT (First Paragraph) +Begin with a gendered noun phrase (e.g., "A woman…", "A man…"). +Include hairstyle and its texture or motion (no color or length). +Include posture, gestures as applicable. +Strictly exclude any reference to ethnicity, age, body type, tattoos, glasses, hair color, hair length, eye color, height, or makeup. + +CINEMATIC AESTHETIC CONTROL (Second Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), optics (lens feel, DOF, rack focus), and exposure/render cues as applicable. + +STYLIZATION & TONE (Third Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.). + +CLOTHING (Fourth Paragraph) +Describe all visible clothing and accessories with absolute certainty and definitiveness. Be specific: identify garment type with confidence, state definitive color(s), material/texture, fit/silhouette, length, notable construction (seams, straps, waistbands), and condition. Include footwear if visible and describe exactly how fabrics respond to motion (stretching, swaying, tightening, wrinkling). Make decisive choices when multiple interpretations are possible - choose one specific description and state it as fact. Do not describe any text, typography, words, letters, logos, brand names, or written content visible on clothing or accessories. Exclude tattoos, glasses, and other prohibited attributes. + +CRITICAL: Output exactly 4 paragraphs, one per category, separated by a blank line. Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact. +``` + +**User Prompt:** + +``` +Please analyze this image and provide a detailed description following the 4-paragraph structure outlined in the system prompt. +``` + +### Text2Image (Images) - Minimal Options (No Subject, No Clothing, No Bokeh) + +**System Prompt:** + +``` +Generate a Wan 2.2 optimized text to image prompt. You are an expert assistant specialized in analyzing and verbalizing input media for instagram-quality posts using the Wan 2.2 Text to Image workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review the provided media. Do not use meta phrases (e.g., "this picture shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +STYLIZATION & TONE (First Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.). + +CRITICAL: Output exactly 1 paragraphs, one per category, separated by a blank line. DO NOT describe clothing, accessories, or garments in any paragraph. DO NOT describe people, subjects, or human figures in any paragraph. Never mention depth of field, bokeh, blur, optics, DOF, rack focus, or any depth-related visual effects. Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact. +``` + +**User Prompt:** + +``` +Please analyze this image and provide a detailed description following the 1-paragraph structure outlined in the system prompt. +``` + +### Text2Image (Images) - Medium Options (Subject + No Clothing + Bokeh) + +**System Prompt:** + +``` +Generate a Wan 2.2 optimized text to image prompt. You are an expert assistant specialized in analyzing and verbalizing input media for instagram-quality posts using the Wan 2.2 Text to Image workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review the provided media. Do not use meta phrases (e.g., "this picture shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +SUBJECT (First Paragraph) +Begin with a gendered noun phrase (e.g., "A woman…", "A man…"). +Include hairstyle and its texture or motion (no color or length). +Include posture, gestures as applicable. +Strictly exclude any reference to ethnicity, age, body type, tattoos, glasses, hair color, hair length, eye color, height, or makeup. + +CINEMATIC AESTHETIC CONTROL (Second Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), optics (lens feel, DOF, rack focus), and exposure/render cues as applicable. + +STYLIZATION & TONE (Third Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.). + +CRITICAL: Output exactly 3 paragraphs, one per category, separated by a blank line. DO NOT describe clothing, accessories, or garments in any paragraph. Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact. +``` + +**User Prompt:** + +``` +Please analyze this image and provide a detailed description following the 3-paragraph structure outlined in the system prompt. +``` + +### ImageEdit (Images) - With Subject + +**System Prompt:** + +``` +You are an expert assistant generating concise, single-sentence Qwen-Image-Edit instructions. Always be completely decisive and definitive - when you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". + +Always begin with "Make this person…", include vivid, focused scene details (e.g. bedroom props, lights, furniture or gym bench, textured wall, window views) early to anchor the setting, describe hairstyle and, outfit style, pose, posture (without age, ethnicity, tattoos, hair color, etc.), include clear torso and head orientation (e.g., "back facing the camera with torso turned 45° and head looking over her shoulder toward viewer"), reference cinematic aesthetic cues (lighting, framing, lens, shot type), anchor realism by stating skin shows subtle pores, light wrinkles, and realistic surface detail, end with "keep everything else unchanged," and include negative safeguards like "no distortion, no blur artifacts." +``` + +**User Prompt:** + +``` +Please analyze this image and generate a single-sentence Qwen-Image-Edit instruction following the guidelines in the system prompt. +``` + +### ImageEdit (Images) - Without Subject + +**System Prompt:** + +``` +You are an expert assistant generating concise, single-sentence Qwen-Image-Edit instructions. Always be completely decisive and definitive - when you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". + +Focus on vivid, focused scene details (e.g. bedroom props, lights, furniture or gym bench, textured wall, window views) to anchor the setting, do not describe people or human subjects, reference cinematic aesthetic cues (lighting, framing, lens, shot type), end with "keep everything else unchanged," and include negative safeguards like "no distortion, no blur artifacts." +``` + +**User Prompt:** + +``` +Please analyze this image and generate a single-sentence Qwen-Image-Edit instruction following the guidelines in the system prompt. +``` + +### Video - All Options Enabled + +**System Prompt:** + +``` +You are an expert assistant specialized in analyzing and verbalizing input videos for cinematic-quality video transformation using the Wan 2.2 + VACE workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review all provided frames as a single clip and infer motion across time; reason stepwise over the entire sequence (start → middle → end). Do not use meta phrases (e.g., "this video shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +1. SUBJECT (First Paragraph) +Begin with a gendered noun phrase (e.g., "A woman…", "A man…"). +Include hairstyle and its texture or motion (no color or length). +Include posture, gestures as applicable. +Strictly exclude any reference to ethnicity, age, body type, tattoos, glasses, hair color, hair length, eye color, height, or makeup. + +2. CLOTHING (Second Paragraph) +Describe all visible clothing and accessories with absolute certainty and definitiveness. Be specific: identify garment type with confidence, state definitive color(s), material/texture, fit/silhouette, length, notable construction (seams, straps, waistbands), and condition. Include footwear if visible and describe exactly how fabrics respond to motion (stretching, swaying, tightening, wrinkling). Make decisive choices when multiple interpretations are possible - choose one specific description and state it as fact. Do not describe any text, typography, words, letters, logos, brand names, or written content visible on clothing or accessories. Exclude tattoos, glasses, and other prohibited attributes. + +3. SCENE (Third Paragraph) +Describe the visible environment clearly and vividly. + +4. MOVEMENT (Fourth Paragraph) +In this paragraph, describe body-part–specific movement and how it aligns with musical rhythm and beat structure. Begin with an overall summary: e.g., 'The subject initiates with a hip sway on the downbeat…'. Then narrate movement chronologically, using precise action verbs and transitions like 'then', 'as', and 'after', referencing the timeline (e.g., early/mid/late beat or second). Specify which body parts move, how they articulate (e.g., 'the right arm lifts upward, then sweeps outward; the torso tilts as the knees bend'), describe footwork, weight shifts, and alignment with music beats. Also include any camera movement (e.g., 'camera pans to follow the torso shift'). Avoid general labels—focus on locomotor and non‑locomotor gestures, repetition, rhythm, and choreography phrasing. Always include any buttock or breast movements that you see + +5. CINEMATIC AESTHETIC CONTROL (Fifth Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), optics (lens feel, DOF, rack focus), and exposure/render cues as applicable. + +6. STYLIZATION & TONE (Sixth Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.). + +CRITICAL: Output exactly 6 paragraphs, one per category, separated by a blank line. Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact. +``` + +**User Prompt:** + +``` +Please analyze this video and provide a detailed description following the 6-paragraph structure outlined in the system prompt. +``` + +### Video - Minimal Options (No Subject, No Clothing, No Bokeh) + +**System Prompt:** + +``` +You are an expert assistant specialized in analyzing and verbalizing input videos for cinematic-quality video transformation using the Wan 2.2 + VACE workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review all provided frames as a single clip and infer motion across time; reason stepwise over the entire sequence (start → middle → end). Do not use meta phrases (e.g., "this video shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +1. SCENE (First Paragraph) +Describe the visible environment clearly and vividly. + +2. MOVEMENT (Second Paragraph) +In this paragraph, describe body-part–specific movement and how it aligns with musical rhythm and beat structure. Begin with an overall summary: e.g., 'The subject initiates with a hip sway on the downbeat…'. Then narrate movement chronologically, using precise action verbs and transitions like 'then', 'as', and 'after', referencing the timeline (e.g., early/mid/late beat or second). Specify which body parts move, how they articulate (e.g., 'the right arm lifts upward, then sweeps outward; the torso tilts as the knees bend'), describe footwork, weight shifts, and alignment with music beats. Also include any camera movement (e.g., 'camera pans to follow the torso shift'). Avoid general labels—focus on locomotor and non‑locomotor gestures, repetition, rhythm, and choreography phrasing. Always include any buttock or breast movements that you see + +3. STYLIZATION & TONE (Third Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.). + +CRITICAL: Output exactly 3 paragraphs, one per category, separated by a blank line. DO NOT describe clothing, accessories, or garments in any paragraph. DO NOT describe people, subjects, or human figures in any paragraph. Never mention depth of field, bokeh, blur, optics, DOF, rack focus, or any depth-related visual effects. Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact. +``` + +**User Prompt:** + +``` +Please analyze this video and provide a detailed description following the 3-paragraph structure outlined in the system prompt. +``` + +## User Prompts (Modules) + +### Subject Module (Paragraph 1 - Images/Videos) + +**Base Subject Prompt:** + +``` +SUBJECT (First Paragraph) +Begin with a gendered noun phrase (e.g., "A woman…", "A man…"). +Include posture, gestures as applicable. +Strictly exclude any reference to ethnicity, age, body type, tattoos, glasses, hair color, hair length, eye color, height, or makeup. +``` + +**Subject Prompt with Hair Style (when describe_hair_style=True):** + +``` +SUBJECT (First Paragraph) +Begin with a gendered noun phrase (e.g., "A woman…", "A man…"). +Include hairstyle and its texture or motion (no color or length). +Include posture, gestures as applicable. +Strictly exclude any reference to ethnicity, age, body type, tattoos, glasses, hair color, hair length, eye color, height, or makeup. +``` + +### Clothing Module (Variable Paragraph Number) + +``` +CLOTHING ({Ordinal} Paragraph) +Describe all visible clothing and accessories with absolute certainty and definitiveness. Be specific: identify garment type with confidence, state definitive color(s), material/texture, fit/silhouette, length, notable construction (seams, straps, waistbands), and condition. Include footwear if visible and describe exactly how fabrics respond to motion (stretching, swaying, tightening, wrinkling). Make decisive choices when multiple interpretations are possible - choose one specific description and state it as fact. Do not describe any text, typography, words, letters, logos, brand names, or written content visible on clothing or accessories. Exclude tattoos, glasses, and other prohibited attributes. +``` + +### Scene Module (Videos Only) + +``` +SCENE ({Ordinal} Paragraph) +Describe the visible environment clearly and vividly. +``` + +### Movement Module (Videos Only) + +``` +MOVEMENT ({Ordinal} Paragraph) +In this paragraph, describe body-part–specific movement and how it aligns with musical rhythm and beat structure. Begin with an overall summary: e.g., 'The subject initiates with a hip sway on the downbeat…'. Then narrate movement chronologically, using precise action verbs and transitions like 'then', 'as', and 'after', referencing the timeline (e.g., early/mid/late beat or second). Specify which body parts move, how they articulate (e.g., 'the right arm lifts upward, then sweeps outward; the torso tilts as the knees bend'), describe footwork, weight shifts, and alignment with music beats. Also include any camera movement (e.g., 'camera pans to follow the torso shift'). Avoid general labels—focus on locomotor and non‑locomotor gestures, repetition, rhythm, and choreography phrasing. Always include any buttock or breast movements that you see +``` + +### Cinematic Aesthetic Module (Variable Paragraph Number) + +**With Bokeh (when describe_bokeh=True):** + +``` +CINEMATIC AESTHETIC CONTROL ({Ordinal} Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), optics (lens feel, DOF, rack focus), and exposure/render cues as applicable. +``` + +**Without Bokeh (when describe_bokeh=False):** + +``` +CINEMATIC AESTHETIC CONTROL ({Ordinal} Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), and exposure/render cues as applicable. Everything must be in sharp focus with no depth of field effects, bokeh, or blur. Do not mention optics, DOF, rack focus, or any depth-related visual effects. +``` + +### Style Module (Final Paragraph) + +``` +STYLIZATION & TONE ({Ordinal} Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.). +``` + +### Critical Notes Module + +**Base Critical Note:** + +``` +CRITICAL: Output exactly {paragraph_count} paragraphs, one per category, separated by a blank line. Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact. +``` + +**Additional Restrictions (conditional):** + +- **When describe_clothing=False:** + ` DO NOT describe clothing, accessories, or garments in any paragraph.` + +- **When describe_subject=False:** + ` DO NOT describe people, subjects, or human figures in any paragraph.` + +- **When describe_bokeh=False:** + ` Never mention depth of field, bokeh, blur, optics, DOF, rack focus, or any depth-related visual effects.` + +## Configuration Options + +The following options control which modules are included and how they behave: + +- **describe_subject** (default: Yes): Whether to include the Subject module +- **describe_clothing** (default: No): Whether to include the Clothing module +- **describe_hair_style** (default: Yes): Whether to include hair descriptions in the Subject module +- **describe_bokeh** (default: Yes): Whether to allow bokeh/depth-of-field descriptions in the Cinematic module +- **model_type**: + - "Text2Image": Uses structured paragraph approach + - "ImageEdit": Uses single-sentence Qwen-Image-Edit format +- **prefix_text**: Text prepended to the final generated description + +## Dynamic Paragraph Numbering + +The system uses dynamic paragraph numbering based on enabled options: + +### Images (Text2Image mode): + +1. **Subject** (if describe_subject=True) +2. **Cinematic Aesthetic Control** (always included) +3. **Stylization & Tone** (always included) +4. **Clothing** (if describe_clothing=True) + +### Videos: + +1. **Subject** (if describe_subject=True) +2. **Clothing** (if describe_clothing=True) +3. **Scene** (always included) +4. **Movement** (always included) +5. **Cinematic Aesthetic Control** (always included) +6. **Stylization & Tone** (always included) + +The paragraph numbers and ordinal names (First, Second, Third, etc.) are automatically calculated and inserted based on which modules are enabled. diff --git a/example_nodes/example_node.py b/example_nodes/example_node.py new file mode 100644 index 0000000..b52ed89 --- /dev/null +++ b/example_nodes/example_node.py @@ -0,0 +1,46 @@ +""" +Example node for ComfyUI-SwissArmyKnife scaffolding. +This demonstrates the basic structure for ComfyUI custom nodes. +""" + + +class ExampleNode: + """ + A minimal example node that demonstrates the basic ComfyUI node structure. + This can be used as a template for creating new nodes. + """ + + @classmethod + def INPUT_TYPES(cls): + """Define the input types for this node.""" + return { + "required": { + "text": ("STRING", {"default": "Hello, ComfyUI!"}), + "number": ("INT", {"default": 42, "min": 0, "max": 100}), + }, + "optional": { + "optional_text": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("output_text",) + FUNCTION = "process" + CATEGORY = "ComfyUI-SwissArmyKnife/Example" + + def process(self, text, number, optional_text=""): + """Process the inputs and return the result.""" + result = f"Text: {text}, Number: {number}" + if optional_text: + result += f", Optional: {optional_text}" + return (result,) + + +# Export the node for ComfyUI registration +NODE_CLASS_MAPPINGS = { + "ExampleNode": ExampleNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ExampleNode": "Example Node", +} \ No newline at end of file diff --git a/package.json b/package.json index 3cc9f89..01fb1ad 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,23 @@ { - "name": "ComfyUI-SwissKnifeUtils", - "version": "1.0.0", - "description": "Random utility nodes for ComfyUI", - "main": "__init__.py", - "scripts": { - "build": "cd ui-react_backup/ui && npm run build", - "watch": "cd ui-react_backup/ui && npm run watch", - "test": "cd web/tests && npm test", - "test:install": "cd web/tests && npm install && npm run install-browser" - }, - "keywords": [ - "comfyui", - "gemini", - "ai", - "custom-nodes" - ], - "author": "Sam Kumar", - "license": "MIT", - "devDependencies": { - "prettier": "^3.6.2" - } + "name": "comfyui-swissarmyknife", + "version": "1.0.0", + "description": "Swiss Army Knife utility nodes for ComfyUI", + "main": "__init__.py", + "scripts": { + "build": "cd ui-react_backup/ui && npm run build", + "watch": "cd ui-react_backup/ui && npm run watch", + "test": "cd web/tests && npm test", + "test:install": "cd web/tests && npm install && npm run install-browser" + }, + "keywords": [ + "comfyui", + "gemini", + "ai", + "custom-nodes" + ], + "author": "Sam Kumar", + "license": "MIT", + "devDependencies": { + "prettier": "^3.6.2" + } } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fb68d72 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,90 @@ +[build-system] +requires = ["setuptools>=70.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "comfyui-swissarmyknife" +version = "0.0.1" +description = "A collection of custom nodes for ComfyUI" +authors = [ + {name = "Sam Kumar", email = "sam@skproductions.llc"} +] +readme = "README.md" +license = {text = "GNU General Public License v3"} +requires-python = ">=3.10" +classifiers = [] +dependencies = [ + "google-genai", + "opencv-python", + "pillow", +] + + + +[project.optional-dependencies] +dev = [ + "black", # code formatting + "bump-my-version", + "coverage", # testing + "mypy", # type checking + "pre-commit", # runs linting on commit + "pytest", # testing + "pytest-cov", # coverage reporting + "ruff", # linting +] + +[project.urls] +Repository = "https://github.com/sammykumar/ComfyUI-SwissArmyKnife" +BugTracker = "https://github.com/sammykumar/ComfyUI-SwissArmyKnife/issues" +Documentation = "https://github.com/sammykumar/ComfyUI-SwissArmyKnife/wiki" + + +[tool.comfy] +PublisherId = "sammykumar" +DisplayName = "ComfyUI-SwissArmyKnife" +Icon = "" +Tags = [] +Repository = "https://github.com/sammykumar/ComfyUI-SwissArmyKnife" +includes = ["dist/"] + +[tool.setuptools.package-data] +"*" = ["*.*"] + +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = [ + "tests", +] + +[tool.mypy] +files = "." + +# Use strict defaults +strict = true +warn_unreachable = true +warn_no_return = true + +[[tool.mypy.overrides]] +# Don't require test functions to include types +module = "tests.*" +allow_untyped_defs = true +disable_error_code = "attr-defined" + +[tool.ruff] +# extend-exclude = ["static", "ci/templates"] +line-length = 140 +src = ["src", "tests"] +target-version = "py39" + +# Add rules to ban exec/eval +[tool.ruff.lint] +select = [ + "S102", # exec-builtin + "S307", # eval-used + "W293", + "F", # The "F" series in Ruff stands for "Pyflakes" rules, which catch various Python syntax errors and undefined names. + # See all rules here: https://docs.astral.sh/ruff/rules/#pyflakes-f +] + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "double" \ No newline at end of file diff --git a/ui-react_backup/__init__.py b/ui-react_backup/__init__.py new file mode 100644 index 0000000..cced937 --- /dev/null +++ b/ui-react_backup/__init__.py @@ -0,0 +1,69 @@ +import os +import server +from aiohttp import web +import folder_paths +import nodes +from .utils.nodes import NODE_CLASS_MAPPINGS +from .utils.nodes import NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] + +# Define the path to our extension +workspace_path = os.path.dirname(__file__) +dist_path = os.path.join(workspace_path, "dist/example_ext") +dist_locales_path = os.path.join(workspace_path, "dist/locales") + +# Print the current paths for debugging +print(f"ComfyUI_example_frontend_extension workspace path: {workspace_path}") +print(f"Dist path: {dist_path}") +print(f"Dist locales path: {dist_locales_path}") +print(f"Locales exist: {os.path.exists(dist_locales_path)}") + +# Register the static route for serving our React app assets +if os.path.exists(dist_path): + # Add the routes for the extension + server.PromptServer.instance.app.add_routes([ + web.static("/example_ext/", dist_path), + ]) + + # Register the locale files route + if os.path.exists(dist_locales_path): + server.PromptServer.instance.app.add_routes([ + web.static("/locales/", dist_locales_path), + ]) + print("Registered locale files route at /locales/") + else: + print("WARNING: Locale directory not found!") + + # Also register the standard ComfyUI extension web directory + + project_name = os.path.basename(workspace_path) + + try: + # Method added in https://github.com/comfyanonymous/ComfyUI/pull/8357 + from comfy_config import config_parser + + project_config = config_parser.extract_node_configuration(workspace_path) + project_name = project_config.project.name + print(f"project name read from pyproject.toml: {project_name}") + except Exception as e: + print(f"Could not load project config, using default name '{project_name}': {e}") + + # Register the dist directory for React app + nodes.EXTENSION_WEB_DIRS[project_name] = os.path.join(workspace_path, "dist") + + # Also register the web directory for JavaScript extensions + web_dir = os.path.join(workspace_path, "web") + if os.path.exists(web_dir): + nodes.EXTENSION_WEB_DIRS[f"{project_name}_web"] = web_dir + print(f"Registered web directory: {web_dir}") + +else: + print("ComfyUI Example React Extension: Dist directory not found") + + # Fallback: register web directory even if dist doesn't exist + project_name = os.path.basename(workspace_path) + web_dir = os.path.join(workspace_path, "web") + if os.path.exists(web_dir): + nodes.EXTENSION_WEB_DIRS[f"{project_name}_web"] = web_dir + print(f"Registered web directory (fallback): {web_dir}") \ No newline at end of file diff --git a/ui-react_backup/ui/.eslintrc.json b/ui-react_backup/ui/.eslintrc.json new file mode 100644 index 0000000..8cab913 --- /dev/null +++ b/ui-react_backup/ui/.eslintrc.json @@ -0,0 +1,35 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:react/jsx-runtime", + "plugin:prettier/recommended" + ], + "ignorePatterns": ["dist", ".eslintrc.json"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react-refresh", "unused-imports"], + "rules": { + "react-refresh/only-export-components": [ + "warn", + { "allowConstantExport": true } + ], + "unused-imports/no-unused-imports": "error", + "@typescript-eslint/no-explicit-any": "off" + }, + "settings": { + "react": { + "version": "detect" + } + } +} \ No newline at end of file diff --git a/ui-react_backup/ui/.prettierrc b/ui-react_backup/ui/.prettierrc new file mode 100644 index 0000000..dcf3390 --- /dev/null +++ b/ui-react_backup/ui/.prettierrc @@ -0,0 +1,18 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "semi": false, + "trailingComma": "none", + "printWidth": 80, + "importOrder": ["^@core/(.*)$", "", "^@/(.*)$", "^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "overrides": [ + { + "files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}", + "options": { + "plugins": ["@trivago/prettier-plugin-sort-imports"] + } + } + ] +} \ No newline at end of file diff --git a/ui-react_backup/ui/eslint.config.js b/ui-react_backup/ui/eslint.config.js new file mode 100644 index 0000000..714f698 --- /dev/null +++ b/ui-react_backup/ui/eslint.config.js @@ -0,0 +1,39 @@ +import pluginJs from '@eslint/js' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import unusedImports from 'eslint-plugin-unused-imports' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default [ + { + files: ['src/**/*.{js,mjs,cjs,ts,tsx}'] + }, + { + languageOptions: { + globals: { + ...globals.browser + }, + parser: tseslint.parser, + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2020, + sourceType: 'module' + } + } + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + eslintPluginPrettierRecommended, + { + plugins: { + 'unused-imports': unusedImports + }, + rules: { + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/prefer-as-const': 'off', + 'unused-imports/no-unused-imports': 'error' + } + } +] \ No newline at end of file diff --git a/ui-react_backup/ui/jest.config.js b/ui-react_backup/ui/jest.config.js new file mode 100644 index 0000000..34b0def --- /dev/null +++ b/ui-react_backup/ui/jest.config.js @@ -0,0 +1,16 @@ +export default { + testEnvironment: 'jsdom', + transform: { + '^.+\\.(ts|tsx)$': ['ts-jest', { + useESM: true, + }], + }, + moduleNameMapper: { + // Handle CSS imports (with CSS modules) + '\\.css$': 'identity-obj-proxy', + }, + extensionsToTreatAsEsm: ['.ts', '.tsx'], + setupFilesAfterEnv: ['/jest.setup.js'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'] +}; \ No newline at end of file diff --git a/ui-react_backup/ui/jest.setup.js b/ui-react_backup/ui/jest.setup.js new file mode 100644 index 0000000..f01581c --- /dev/null +++ b/ui-react_backup/ui/jest.setup.js @@ -0,0 +1,16 @@ +// Import jest-dom additions +import '@testing-library/jest-dom'; + +// Mock window.app for ComfyUI integration testing +global.window.app = { + graph: { + _nodes: [] + }, + api: { + addEventListener: jest.fn(), + removeEventListener: jest.fn() + }, + canvas: { + centerOnNode: jest.fn() + } +}; \ No newline at end of file diff --git a/ui-react_backup/ui/package-lock.json b/ui-react_backup/ui/package-lock.json new file mode 100644 index 0000000..7b08e81 --- /dev/null +++ b/ui-react_backup/ui/package-lock.json @@ -0,0 +1,8825 @@ +{ + "name": "comfyui-swissarmyknife-ui", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "comfyui-swissarmyknife-ui", + "version": "0.0.1", + "dependencies": { + "i18next": "^23.10.2", + "i18next-browser-languagedetector": "^7.2.2", + "i18next-http-backend": "^2.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.1.0" + }, + "devDependencies": { + "@comfyorg/comfyui-frontend-types": "^1.20.2", + "@eslint/eslintrc": "^3.0.2", + "@eslint/js": "^9.27.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/jest": "^29.5.14", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/parser": "^8.32.1", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^9.27.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.4.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.20", + "eslint-plugin-unused-imports": "^4.1.4", + "globals": "^16.1.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.5.3", + "ts-jest": "^29.3.4", + "typescript": "^5.4.2", + "typescript-eslint": "^8.32.1", + "vite": "^5.2.10" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@comfyorg/comfyui-frontend-types": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@comfyorg/comfyui-frontend-types/-/comfyui-frontend-types-1.20.2.tgz", + "integrity": "sha512-UcQXfLLGueqQ/U7kAwryO4N+97HPYkB/rb+QTm9IqRorAA+rtHEVWX+Y2IEYib/b5j/s0Am+xbi/CXQmA2ZFbQ==", + "dev": true, + "dependencies": { + "@comfyorg/litegraph": "^0.15.11" + }, + "peerDependencies": { + "vue": "^3.5.13", + "zod": "^3.23.8" + } + }, + "node_modules/@comfyorg/litegraph": { + "version": "0.15.11", + "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.11.tgz", + "integrity": "sha512-gU8KK9cid7dXSK1yh3ReUolG0HGT3piKgKLd8YDr21PWl64pQvzy8BIh7W1vKH8ZWictKmNBaG9IRKlsJ667Zw==", + "dev": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.14.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "peer": true + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "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, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", + "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.15.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.19.tgz", + "integrity": "sha512-3vMNr4TzNQyjHcRZadojpRaD9Ofr6LsonZAoQ+HMUa/9ORTPoxVIw0e0mpqWpdjj8xybyCM+oKOUH2vwFu/oEw==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.21.tgz", + "integrity": "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dev": true, + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz", + "integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.27.2", + "@vue/shared": "3.5.16", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz", + "integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==", + "dev": true, + "peer": true, + "dependencies": { + "@vue/compiler-core": "3.5.16", + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz", + "integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.27.2", + "@vue/compiler-core": "3.5.16", + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.3", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz", + "integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==", + "dev": true, + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.16", + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz", + "integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==", + "dev": true, + "peer": true, + "dependencies": { + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz", + "integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==", + "dev": true, + "peer": true, + "dependencies": { + "@vue/reactivity": "3.5.16", + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz", + "integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==", + "dev": true, + "peer": true, + "dependencies": { + "@vue/reactivity": "3.5.16", + "@vue/runtime-core": "3.5.16", + "@vue/shared": "3.5.16", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz", + "integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==", + "dev": true, + "peer": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16" + }, + "peerDependencies": { + "vue": "3.5.16" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz", + "integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==", + "dev": true, + "peer": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "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, + "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/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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 + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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 + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "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, + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, + "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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "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, + "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 + }, + "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==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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 + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "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, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "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, + "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==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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 + }, + "node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", + "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", + "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "dev": true, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "peer": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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 + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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 + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.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, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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 + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "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==", + "dev": true, + "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-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "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, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/i18next": { + "version": "23.16.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", + "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.2.tgz", + "integrity": "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.7.3.tgz", + "integrity": "sha512-FgZxrXdRA5u44xfYsJlEBL4/KH3f2IluBpgV/7riW0YW2VEyM8FzVt2XHAOi6id0Ppj7vZvCZVpp5LrGXnc8Ig==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "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, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "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==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "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==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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 + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "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 + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": 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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": 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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "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 + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-i18next": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.3.tgz", + "integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", + "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.0", + "@rollup/rollup-android-arm64": "4.41.0", + "@rollup/rollup-darwin-arm64": "4.41.0", + "@rollup/rollup-darwin-x64": "4.41.0", + "@rollup/rollup-freebsd-arm64": "4.41.0", + "@rollup/rollup-freebsd-x64": "4.41.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", + "@rollup/rollup-linux-arm-musleabihf": "4.41.0", + "@rollup/rollup-linux-arm64-gnu": "4.41.0", + "@rollup/rollup-linux-arm64-musl": "4.41.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-musl": "4.41.0", + "@rollup/rollup-linux-s390x-gnu": "4.41.0", + "@rollup/rollup-linux-x64-gnu": "4.41.0", + "@rollup/rollup-linux-x64-musl": "4.41.0", + "@rollup/rollup-win32-arm64-msvc": "4.41.0", + "@rollup/rollup-win32-ia32-msvc": "4.41.0", + "@rollup/rollup-win32-x64-msvc": "4.41.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/synckit": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.6.tgz", + "integrity": "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz", + "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", + "dev": true, + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-sfc": "3.5.16", + "@vue/runtime-dom": "3.5.16", + "@vue/server-renderer": "3.5.16", + "@vue/shared": "3.5.16" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "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 + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "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/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "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, + "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, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.42", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.42.tgz", + "integrity": "sha512-PcALTLskaucbeHc41tU/xfjfhcz8z0GdhhDcSgrCTmSazUuqnYqiXO63M0QUBVwpBlsLsNVn5qHSC5Dw3KZvaQ==", + "dev": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/ui-react_backup/ui/package.json b/ui-react_backup/ui/package.json new file mode 100644 index 0000000..8941251 --- /dev/null +++ b/ui-react_backup/ui/package.json @@ -0,0 +1,55 @@ +{ + "name": "comfyui-swissarmyknife-ui", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "watch": "tsc && vite build --watch", + "lint": "eslint . --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --ext ts,tsx,js,jsx --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"", + "typecheck": "tsc --noEmit", + "preview": "vite preview", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "i18next": "^23.10.2", + "i18next-browser-languagedetector": "^7.2.2", + "i18next-http-backend": "^2.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.1.0" + }, + "devDependencies": { + "@comfyorg/comfyui-frontend-types": "^1.20.2", + "@eslint/eslintrc": "^3.0.2", + "@eslint/js": "^9.27.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/jest": "^29.5.14", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/parser": "^8.32.1", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^9.27.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.4.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.20", + "eslint-plugin-unused-imports": "^4.1.4", + "globals": "^16.1.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.5.3", + "ts-jest": "^29.3.4", + "typescript": "^5.4.2", + "typescript-eslint": "^8.32.1", + "vite": "^5.2.10" + } +} diff --git a/ui-react_backup/ui/public/locales/en/main.json b/ui-react_backup/ui/public/locales/en/main.json new file mode 100644 index 0000000..98f2d5c --- /dev/null +++ b/ui-react_backup/ui/public/locales/en/main.json @@ -0,0 +1,24 @@ +{ + "app": { + "title": "React Example Extension", + "description": "Shows statistics about nodes in the current workflow", + "noNodes": "No nodes in the workflow", + "nodeList": { + "title": "Node List", + "id": "ID", + "type": "Type", + "category": "Category", + "inputs": "Inputs", + "outputs": "Outputs" + }, + "nodeStats": { + "title": "Node Statistics", + "totalNodes": "Total nodes", + "uniqueNodeTypes": "Unique node types" + }, + "footer": { + "clickToHighlight": "Click on any node in the list to highlight it in the workflow" + } + }, + "Click on any node in the list to highlight it in the workflow": "Click on any node in the list to highlight it in the workflow" +} \ No newline at end of file diff --git a/ui-react_backup/ui/public/locales/zh/main.json b/ui-react_backup/ui/public/locales/zh/main.json new file mode 100644 index 0000000..e1ff485 --- /dev/null +++ b/ui-react_backup/ui/public/locales/zh/main.json @@ -0,0 +1,24 @@ +{ + "app": { + "title": "React 示例扩展", + "description": "显示当前工作流中节点的统计信息", + "noNodes": "工作流中没有节点", + "nodeList": { + "title": "节点列表", + "id": "ID", + "type": "类型", + "category": "类别", + "inputs": "输入", + "outputs": "输出" + }, + "nodeStats": { + "title": "节点统计", + "totalNodes": "节点总数", + "uniqueNodeTypes": "唯一节点类型" + }, + "footer": { + "clickToHighlight": "单击列表中的任何节点以在工作流中突出显示它" + } + }, + "Click on any node in the list to highlight it in the workflow": "单击列表中的任何节点以在工作流中突出显示它" +} \ No newline at end of file diff --git a/ui-react_backup/ui/src/App.css b/ui-react_backup/ui/src/App.css new file mode 100644 index 0000000..dd3e415 --- /dev/null +++ b/ui-react_backup/ui/src/App.css @@ -0,0 +1,234 @@ +.react-example-container { + padding: 16px; + height: 100%; + max-height: 100vh; + display: flex; + flex-direction: column; + background-color: var(--example-bg-color); + color: var(--example-text-color); + overflow-y: auto; +} + +.react-example-container h2 { + margin-top: 0; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--example-border-color); + color: var(--example-primary-color); +} + +.react-example-container h3 { + margin-top: 0; + margin-bottom: 12px; + color: var(--example-text-color); + font-size: 1.1em; +} + +/* Stats Overview */ +.stats-overview { + display: flex; + gap: 12px; + margin-bottom: 20px; +} + +.stat-card { + flex: 1; + padding: 12px; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 4px; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.stat-value { + font-size: 24px; + font-weight: bold; + color: var(--example-primary-color); +} + +.stat-label { + font-size: 12px; + color: var(--example-muted-text-color); + margin-top: 4px; +} + +/* Dashboard Sections */ +.dashboard-section { + margin-bottom: 20px; + padding: 16px; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +/* Node Chart */ +.node-stats-chart { + display: flex; + height: 120px; + align-items: flex-end; + gap: 12px; + margin-top: 16px; + padding-bottom: 24px; + position: relative; +} + +.chart-bar-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} + +.chart-bar { + width: 24px; + min-height: 4px; + border-radius: 4px 4px 0 0; + transition: height 0.3s ease; +} + +.chart-label { + position: absolute; + bottom: -24px; + font-size: 10px; + color: var(--example-muted-text-color); + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + transform: rotate(-45deg); + transform-origin: top left; + margin-left: 12px; +} + +/* Category Filter */ +.category-filter { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; +} + +.filter-button { + background-color: transparent; + border: none; + color: var(--example-text-color); + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.2s; + border-bottom: 3px solid transparent; +} + +.filter-button:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.filter-button.active { + background-color: rgba(255, 255, 255, 0.1); + font-weight: bold; +} + +/* Node List */ +.node-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 150px; + overflow-y: auto; + padding-right: 8px; +} + +.node-item { + display: flex; + align-items: center; + padding: 8px 12px; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 4px; + cursor: pointer; + transition: + transform 0.2s, + background-color 0.2s; +} + +.node-item:hover { + background-color: rgba(0, 0, 0, 0.2); + transform: translateX(2px); +} + +.node-badge { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 12px; +} + +.node-title { + flex: 1; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.node-meta { + display: flex; + gap: 8px; + font-size: 11px; + color: var(--example-muted-text-color); +} + +/* Empty State */ +.empty-state { + padding: 20px; + text-align: center; + color: var(--example-muted-text-color); + font-style: italic; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +/* Footer */ +.footer { + margin-top: auto; + padding-top: 16px; + border-top: 1px solid var(--example-border-color); + font-size: 0.8em; + text-align: center; + color: var(--example-muted-text-color); +} + +/* API Examples Section */ +.api-examples { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} + +.api-examples h4 { + width: 100%; + margin-top: 10px; + margin-bottom: 5px; + color: var(--example-text-color); +} + +/* Scrollbar styling */ +.node-list::-webkit-scrollbar { + width: 6px; +} + +.node-list::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 3px; +} + +.node-list::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.node-list::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} diff --git a/ui-react_backup/ui/src/App.tsx b/ui-react_backup/ui/src/App.tsx new file mode 100644 index 0000000..dc83072 --- /dev/null +++ b/ui-react_backup/ui/src/App.tsx @@ -0,0 +1,543 @@ +import { ComfyApp } from '@comfyorg/comfyui-frontend-types' +import { useEffect, useMemo, useState } from 'react' + +import { useTranslation } from '../node_modules/react-i18next' +import './App.css' + +// Type definitions for the global ComfyUI objects +declare global { + interface Window { + app?: ComfyApp + } +} + +// Interface for our processed node data +interface ProcessedNode { + id: string | number + title: string + type: string + category: string + inputs: number + outputs: number + pos: [number, number] +} + +// Type for category colors +type CategoryColors = Record + +const CATEGORY_COLORS: CategoryColors = { + loaders: '#7e57c2', + conditioning: '#26a69a', + sampling: '#ef5350', + latent: '#66bb6a', + image: '#42a5f5', + mask: '#ff9800', + 'conditioning/clip': '#26a69a', + 'image/postprocessing': '#ec407a', + advanced: '#5c6bc0', + _default: '#78909c' +} + +interface NodeStatsChartProps { + nodeCounts: Record + totalNodes: number +} + +function NodeStatsChart({ nodeCounts, totalNodes }: NodeStatsChartProps) { + // Only show the top 8 categories for the chart + const topCategories = Object.entries(nodeCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 8) + + const chartData = topCategories.map(([category, count]) => { + return { + category, + count, + percentage: Math.round((count / totalNodes) * 100) + } + }) + + return ( +
+ {chartData.map((item) => ( +
+
+
{item.category}
+
+ ))} +
+ ) +} + +interface CategoryFilterProps { + categories: string[] + selectedCategory: string + onSelectCategory: (category: string) => void +} + +function CategoryFilter({ + categories, + selectedCategory, + onSelectCategory +}: CategoryFilterProps) { + // Using useTranslation hook to initialize i18n context + useTranslation() + + return ( +
+ + {categories.map((category) => ( + + ))} +
+ ) +} + +function App() { + const { t } = useTranslation() + const [nodes, setNodes] = useState([]) + const [selectedCategory, setSelectedCategory] = useState('all') + const [isQueueRunning, setIsQueueRunning] = useState(false) + const [highlightedNode, setHighlightedNode] = useState< + string | number | null + >(null) + + // Get nodes from ComfyUI graph and organize them + useEffect(() => { + if (!window.app?.graph) return + + // Initial node collection + collectNodes() + + // Function to collect nodes from the graph + function collectNodes() { + const graphNodes = window.app?.graph._nodes + if (!graphNodes) return + + const currentNodes: ProcessedNode[] = [] + + for (const node of graphNodes) { + // Extract category from constructorData if available + let category = '' + if (node.constructor && 'nodeData' in node.constructor) { + const nodeData = node.constructor.nodeData + if ( + nodeData && + typeof nodeData === 'object' && + 'category' in nodeData + ) { + category = nodeData.category as string + } + } + + currentNodes.push({ + id: node.id, + title: node.title, + type: node.type, + category: category, + inputs: node.inputs?.length || 0, + outputs: node.outputs?.length || 0, + pos: [...node.pos] as [number, number] // Clone position array + }) + } + + setNodes(currentNodes) + } + + // Listen for graph changes using the API (not graph directly) + const handleGraphChanged = () => { + collectNodes() + } + + window.app?.api.addEventListener('graphChanged', handleGraphChanged) + + return () => { + window.app?.api.removeEventListener('graphChanged', handleGraphChanged) + } + }, []) + + // Monitor queue status + useEffect(() => { + if (!window.app?.api) return + + const handleQueueStart = () => setIsQueueRunning(true) + const handleQueueComplete = () => setIsQueueRunning(false) + + // Using the Event API with properly typed events + window.app.api.addEventListener('execution_start', handleQueueStart) + + // Since 'execution_complete' is not directly in the types, we add a compatibility approach + type ApiEventName = Parameters[0] + window.app.api.addEventListener( + 'execution_complete' as ApiEventName, + handleQueueComplete + ) + + return () => { + window.app?.api.removeEventListener('execution_start', handleQueueStart) + window.app?.api.removeEventListener( + 'execution_complete' as ApiEventName, + handleQueueComplete + ) + } + }, []) + + // Handle node highlighting + useEffect(() => { + if (!highlightedNode || !window.app?.graph) return + + // Find the node in the graph + const graphNodes = window.app.graph._nodes + const node = graphNodes.find((n) => n.id === highlightedNode) + if (!node) return + + // Center camera on node with smooth animation + window.app.canvas.centerOnNode(node) + + // Highlight the node + const originalColor = node.color + node.color = '#ff5722' + window.app.graph.setDirtyCanvas(true, false) + + // Reset highlight after a delay + const timeout = setTimeout(() => { + if (window.app?.graph._nodes.includes(node)) { + node.color = originalColor + window.app.graph.setDirtyCanvas(true, false) + } + }, 2000) + + return () => { + clearTimeout(timeout) + // Restore original color if component unmounts + if (window.app?.graph._nodes.includes(node)) { + node.color = originalColor + window.app.graph.setDirtyCanvas(true, false) + } + } + }, [highlightedNode]) + + // Calculate statistics from nodes + const { filteredNodes, categories, nodeCounts, totalNodes } = useMemo(() => { + // Get filtered nodes based on selected category + const filtered = + selectedCategory === 'all' + ? nodes + : nodes.filter((node) => node.category === selectedCategory) + + // Get unique categories + const uniqueCategories = [ + ...new Set(nodes.map((node) => node.category)) + ].sort() + + // Count nodes by category + const counts = nodes.reduce((acc: Record, node) => { + const category = node.category + acc[category] = (acc[category] || 0) + 1 + return acc + }, {}) + + return { + filteredNodes: filtered, + categories: uniqueCategories, + nodeCounts: counts, + totalNodes: nodes.length + } + }, [nodes, selectedCategory]) + + return ( +
+

{t('app.title')}

+ +
+
+
{totalNodes}
+
{t('app.nodeStats.totalNodes')}
+
+
+
{categories.length}
+
{t('app.nodeStats.uniqueNodeTypes')}
+
+
+
{isQueueRunning ? 'Active' : 'Idle'}
+
Queue Status
+
+
+ +
+

Node Type Distribution

+ {totalNodes > 0 ? ( + + ) : ( +
{t('app.noNodes')}
+ )} +
+ +
+

{t('app.nodeList.title')}

+ + +
+ {filteredNodes.length > 0 ? ( + filteredNodes.map((node) => ( +
setHighlightedNode(node.id)} + > +
+
{node.title}
+
+ + {t('app.nodeList.inputs')}: {node.inputs} + + + {t('app.nodeList.outputs')}: {node.outputs} + +
+
+ )) + ) : ( +
+ {totalNodes === 0 + ? t('app.noNodes') + : 'No nodes match the selected filter'} +
+ )} +
+
+ +
+

API Examples

+
+

Dialog API

+ + + + +

Toast API

+ + + + + + + + + +
+
+ +
+

{t('app.footer.clickToHighlight')}

+
+
+ ) +} + +export default App diff --git a/ui-react_backup/ui/src/__tests__/dummy.test.tsx b/ui-react_backup/ui/src/__tests__/dummy.test.tsx new file mode 100644 index 0000000..e56ce61 --- /dev/null +++ b/ui-react_backup/ui/src/__tests__/dummy.test.tsx @@ -0,0 +1,18 @@ +/** @jsxImportSource react */ +import '@testing-library/jest-dom' +import { render } from '@testing-library/react' + +// A simple component for testing +function DummyComponent({ text }: { text: string }) { + return
{text}
+} + +describe('Dummy Component', () => { + it('renders with provided text', () => { + const { getByTestId } = render() + + const element = getByTestId('dummy-component') + expect(element).toBeInTheDocument() + expect(element).toHaveTextContent('Hello ComfyUI!') + }) +}) diff --git a/ui-react_backup/ui/src/index.css b/ui-react_backup/ui/src/index.css new file mode 100644 index 0000000..f0f9f36 --- /dev/null +++ b/ui-react_backup/ui/src/index.css @@ -0,0 +1,33 @@ +/* Base styles for our extension */ +:root { + --example-primary-color: #3498db; + --example-secondary-color: #2c3e50; + --example-text-color: #ecf0f1; + --example-accent-color: #e74c3c; + --example-bg-color: #34495e; + --example-border-color: #7f8c8d; + --example-muted-text-color: #95a5a6; +} + +/* Light mode overrides */ +:root.comfy-theme-light #comfyui-react-example-root { + --example-primary-color: #2980b9; + --example-secondary-color: #34495e; + --example-text-color: #2c3e50; + --example-accent-color: #c0392b; + --example-bg-color: #ecf0f1; + --example-border-color: #bdc3c7; + --example-muted-text-color: #7f8c8d; +} + +/* Make sure our styles don't leak out to the rest of ComfyUI */ +#comfyui-react-example-root { + height: 100%; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + box-sizing: border-box; + color: var(--example-text-color); +} + +#comfyui-react-example-root * { + box-sizing: border-box; +} diff --git a/ui-react_backup/ui/src/main.tsx b/ui-react_backup/ui/src/main.tsx new file mode 100644 index 0000000..298c971 --- /dev/null +++ b/ui-react_backup/ui/src/main.tsx @@ -0,0 +1,243 @@ +import { ComfyApp } from '@comfyorg/comfyui-frontend-types' +import React, { Suspense } from 'react' +import ReactDOM from 'react-dom/client' + +import { useTranslation } from '../node_modules/react-i18next' +import './index.css' +import './utils/i18n' + +// Declare global ComfyUI objects +declare global { + interface Window { + app?: ComfyApp + } +} + +// Lazy load the App component for better performance +const App = React.lazy(() => import('./App')) + +// Function to wait for document and app to be ready +function waitForInit(): Promise { + return new Promise((resolve) => { + // Check if document is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', checkApp) + } else { + checkApp() + } + + // Check if app is available + function checkApp() { + if (window.app) { + resolve() + } else { + // Poll for app availability + const interval = setInterval(() => { + if (window.app) { + console.log('App initialized') + clearInterval(interval) + resolve() + } + }, 50) + + // Set timeout to avoid infinite polling + setTimeout(() => { + clearInterval(interval) + console.error('Timeout waiting for app to initialize') + resolve() // Continue anyway to avoid blocking + }, 5000) + } + } + }) +} + +// Initialize the extension once everything is ready +async function initializeExtension(): Promise { + try { + // Wait for document and ComfyUI app + await waitForInit() + console.log('App:', window.app) + + if (!window.app) { + console.error('ComfyUI app not available') + return + } + + // Create a function component with i18n for translation + function SidebarWrapper() { + // Using useTranslation hook to initialize i18n context + useTranslation() + return + } + + // Register the sidebar tab using ComfyUI's extension API + const sidebarTab = { + id: 'comfyui-react-example', + icon: 'pi pi-code', // Using PrimeVue icon + title: 'React Example', + tooltip: 'React Example Extension', + type: 'custom' as const, + render: (element: HTMLElement) => { + console.log('Rendering React Example Extension') + // Create a container for our React app + const container = document.createElement('div') + container.id = 'comfyui-react-example-root' + container.style.height = '100%' + element.appendChild(container) + + // Mount the React app to the container + ReactDOM.createRoot(container).render( + + Loading...
}> + + + + ) + } + } + + window.app.extensionManager.registerSidebarTab(sidebarTab) + + // Register extension with about page badges + window.app.registerExtension({ + name: 'ReactExtensionExample', + // About Panel Badges API - Adds custom badges to the ComfyUI about page + aboutPageBadges: [ + { + label: 'Documentation', + url: 'https://docs.comfy.org/custom-nodes/js/javascript_overview', + icon: 'pi pi-file' + }, + { + label: 'GitHub', + url: 'https://github.com/Comfy-Org/ComfyUI-React-Extension-Template', + icon: 'pi pi-github' + }, + { + label: 'Support', + url: 'https://discord.gg/comfy-org', + icon: 'pi pi-discord' + } + ], + + // Bottom Panel Tabs API - Adds custom tabs to the bottom panel + bottomPanelTabs: [ + { + id: 'react-example-tab', + title: 'React Example Tab', + type: 'custom', + render: (el) => { + // Create a container for our React content + const container = document.createElement('div') + container.id = 'react-example-bottom-tab' + container.style.padding = '10px' + el.appendChild(container) + + // Create a React component for the tab content + function TabContent() { + const [count, setCount] = React.useState(0) + + return ( +
+

React Example Bottom Panel

+

This is a demo of the Bottom Panel Tabs API.

+

Count: {count}

+ +
+ ) + } + + // Mount the React component + ReactDOM.createRoot(container).render( + + + + ) + } + } + ], + + // Commands and Keybindings API - Register custom commands with keyboard shortcuts + commands: [ + { + id: 'reactExample.showInfo', + label: 'Show React Example Info', + function: () => { + alert( + 'React Example Extension - This command was triggered by the Commands API' + ) + } + }, + { + id: 'reactExample.runWorkflow', + label: 'Run Workflow from React Example', + function: () => { + void window.app?.queuePrompt(0) // Pass 0 as default prompt number + } + }, + { + id: 'reactExample.clearWorkflow', + label: 'Clear Workflow from React Example', + function: () => { + if (confirm('Clear the current workflow? This cannot be undone.')) { + window.app?.graph.clear() + } + } + } + ], + + // Associate keybindings with the commands + keybindings: [ + { + combo: { key: 'i', ctrl: true, alt: true }, + commandId: 'reactExample.showInfo' + }, + { + combo: { key: 'r', ctrl: true, alt: true }, + commandId: 'reactExample.runWorkflow' + }, + { + combo: { key: 'Delete', ctrl: true, alt: true }, + commandId: 'reactExample.clearWorkflow' + } + ], + + // Topbar Menu API - Add commands to the top menu bar + menuCommands: [ + { + // Add commands to the Extensions menu + path: ['Extensions', 'React Example'], + commands: [ + 'reactExample.showInfo', + 'reactExample.runWorkflow', + 'reactExample.clearWorkflow' + ] + }, + { + // Create a submenu under Extensions > React Example + path: ['Extensions', 'React Example', 'Advanced'], + commands: ['reactExample.showInfo'] + } + ] + }) + + console.log('React Example Extension initialized successfully') + } catch (error) { + console.error('Failed to initialize React Example Extension:', error) + } +} + +// Start initialization +void initializeExtension() diff --git a/ui-react_backup/ui/src/setupTests.ts b/ui-react_backup/ui/src/setupTests.ts new file mode 100644 index 0000000..36488f8 --- /dev/null +++ b/ui-react_backup/ui/src/setupTests.ts @@ -0,0 +1,2 @@ +// Import jest-dom matchers +import '@testing-library/jest-dom' diff --git a/ui-react_backup/ui/src/utils/i18n.ts b/ui-react_backup/ui/src/utils/i18n.ts new file mode 100644 index 0000000..95d04da --- /dev/null +++ b/ui-react_backup/ui/src/utils/i18n.ts @@ -0,0 +1,136 @@ +import i18n from 'i18next' +import LanguageDetector from 'i18next-browser-languagedetector/cjs' +import Backend from 'i18next-http-backend' + +import { initReactI18next } from '../../node_modules/react-i18next' + +// Define fallback translations for debugging +interface TranslationResource { + [language: string]: { + [namespace: string]: { + app: { + title: string + description: string + noNodes: string + nodeList: { + title: string + id: string + type: string + category: string + inputs: string + outputs: string + } + nodeStats: { + title: string + totalNodes: string + uniqueNodeTypes: string + } + footer: { + clickToHighlight: string + } + } + } + } +} + +const fallbackResources: TranslationResource = { + en: { + main: { + app: { + title: 'React Example Extension (Fallback)', + description: 'Shows statistics about nodes in the current workflow', + noNodes: 'No nodes in the workflow', + nodeList: { + title: 'Node List', + id: 'ID', + type: 'Type', + category: 'Category', + inputs: 'Inputs', + outputs: 'Outputs' + }, + nodeStats: { + title: 'Node Statistics', + totalNodes: 'Total nodes', + uniqueNodeTypes: 'Unique node types' + }, + footer: { + clickToHighlight: + 'Click on any node in the list to highlight it in the workflow' + } + } + } + } +} + +// Initialize i18next +void i18n + // Load translations via http backend (must be first!) + .use(Backend) + // Detect user language + .use(LanguageDetector) + // Initialize react-i18next + .use(initReactI18next) + // Initialize i18next + .init({ + // Always enable debug mode to see what's happening + debug: true, + + // Fallback language + fallbackLng: 'en', + + // Namespace for translations + ns: ['main'], + defaultNS: 'main', + + // Do not load from bundled resources first + initImmediate: true, + + // Custom handling for missing keys + saveMissing: true, + missingKeyHandler: (lng, ns, key) => { + console.log(`Missing translation: [${lng}] ${ns}:${key}`) + }, + + // Language detection configuration + detection: { + // Order of detection methods + order: ['navigator', 'htmlTag', 'path', 'subdomain'], + // Don't cache detected language + caches: [] + }, + + // Backend configuration + backend: { + // Path to load translations from + loadPath: '/locales/{{lng}}/{{ns}}.json', + // Add retry logic + requestOptions: { + retry: 3, + timeout: 3000 + } + }, + + // React specific options + react: { + useSuspense: true + }, + + // Allow string formatting for dynamic values + interpolation: { + escapeValue: false // Not needed for React as it escapes by default + } + }) + +// Add fallback resources only if HTTP loading fails +i18n.on('failedLoading', (lng, ns) => { + console.log( + `Failed loading translation file for ${lng} and ${ns}, using fallback` + ) + + // Add the fallback resources for this language + if (fallbackResources[lng] && fallbackResources[lng][ns]) { + i18n.addResourceBundle(lng, ns, fallbackResources[lng][ns], true, true) + } +}) + +export default i18n diff --git a/ui-react_backup/ui/src/vite-env.d.ts b/ui-react_backup/ui/src/vite-env.d.ts new file mode 100644 index 0000000..d0692c6 --- /dev/null +++ b/ui-react_backup/ui/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMeta { + env: { + DEV: boolean + PROD: boolean + MODE: string + } +} diff --git a/ui-react_backup/ui/tsconfig.json b/ui-react_backup/ui/tsconfig.json new file mode 100644 index 0000000..9a66ff9 --- /dev/null +++ b/ui-react_backup/ui/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Testing */ + "types": ["jest", "@testing-library/jest-dom"] + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/ui-react_backup/ui/tsconfig.node.json b/ui-react_backup/ui/tsconfig.node.json new file mode 100644 index 0000000..099658c --- /dev/null +++ b/ui-react_backup/ui/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/ui-react_backup/ui/vite.config.js b/ui-react_backup/ui/vite.config.js new file mode 100644 index 0000000..6cee9d7 --- /dev/null +++ b/ui-react_backup/ui/vite.config.js @@ -0,0 +1,63 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +// Plugin to correctly handle the ComfyUI scripts in development mode +const rewriteComfyImports = ({ isDev }) => { + return { + name: "rewrite-comfy-imports", + resolveId(source) { + if (!isDev) { + return; + } + if (source === "/scripts/app.js") { + return "http://127.0.0.1:8188/scripts/app.js"; + } + if (source === "/scripts/api.js") { + return "http://127.0.0.1:8188/scripts/api.js"; + } + return null; + }, + }; +}; + +// Plugin to copy locales to the output directory +const copyLocales = () => { + return { + name: "copy-locales", + writeBundle() { + // This runs after bundle is written + console.log("Bundle complete, copying locales..."); + } + }; +}; + +export default defineConfig(({ mode }) => ({ + plugins: [ + react(), + rewriteComfyImports({ isDev: mode === "development" }), + copyLocales() + ], + publicDir: "public", // Explicitly set public directory + build: { + emptyOutDir: true, + rollupOptions: { + // Don't bundle ComfyUI scripts - they will be loaded from the ComfyUI server + external: ['/scripts/app.js', '/scripts/api.js'], + input: { + main: path.resolve(__dirname, 'src/main.tsx'), + }, + output: { + // Output to the dist/example_ext directory + dir: '../dist', + entryFileNames: 'example_ext/[name].js', + chunkFileNames: 'example_ext/[name]-[hash].js', + assetFileNames: 'example_ext/[name][extname]', + // Split React into a separate vendor chunk for better caching + manualChunks: { + 'vendor': ['react', 'react-dom'], + } + } + } + } +})); \ No newline at end of file diff --git a/ui-react_backup/ui/vite.config.ts b/ui-react_backup/ui/vite.config.ts new file mode 100644 index 0000000..15e2675 --- /dev/null +++ b/ui-react_backup/ui/vite.config.ts @@ -0,0 +1,54 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +interface RewriteComfyImportsOptions { + isDev: boolean; +} + +// Plugin to correctly handle the ComfyUI scripts in development mode +const rewriteComfyImports = ({ isDev }: RewriteComfyImportsOptions) => { + return { + name: "rewrite-comfy-imports", + resolveId(source: string) { + if (!isDev) { + return; + } + if (source === "/scripts/app.js") { + return "http://127.0.0.1:8188/scripts/app.js"; + } + if (source === "/scripts/api.js") { + return "http://127.0.0.1:8188/scripts/api.js"; + } + return null; + }, + }; +}; + +export default defineConfig(({ mode }) => ({ + plugins: [ + react(), + rewriteComfyImports({ isDev: mode === "development" }) + ], + build: { + emptyOutDir: true, + rollupOptions: { + // Don't bundle ComfyUI scripts - they will be loaded from the ComfyUI server + external: ['/scripts/app.js', '/scripts/api.js'], + input: { + main: path.resolve(__dirname, 'src/main.tsx'), + }, + output: { + // Output to the dist/example_ext directory + dir: '../dist', + entryFileNames: 'example_ext/[name].js', + chunkFileNames: 'example_ext/[name]-[hash].js', + assetFileNames: 'example_ext/[name][extname]', + // Split React into a separate vendor chunk for better caching + manualChunks: { + 'vendor': ['react', 'react-dom'], + } + } + } + } +})) \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/cache.py b/utils/cache.py new file mode 100644 index 0000000..75e68ce --- /dev/null +++ b/utils/cache.py @@ -0,0 +1,207 @@ +""" +Cache utility for Gemini media descriptions. + +Provides transparent caching of Gemini API responses based on media content and prompts. +Cache keys combine media identifiers with configurable option flags to ensure unique storage +per media+prompt combination. +""" + +import os +import json +import hashlib +import time +from typing import Optional, Dict, Any + + +class GeminiCache: + """ + Simple file-based cache for Gemini media descriptions. + + Cache key format: hash(media_identifier + gemini_model + model_type + options_hash) + Where: + - media_identifier is file_path+mtime for files, or content_hash for tensors + - gemini_model is the model name (e.g., "models/gemini-2.5-flash") + - model_type is for images only (e.g., "Text2Image", "ImageEdit") + - options_hash is an MD5 hash of the JSON-serialized options dictionary + + This design scales to unlimited options without requiring code changes. + """ + + def __init__(self, cache_dir: Optional[str] = None): + """Initialize cache with specified directory.""" + if cache_dir is None: + # Use a cache directory in the same location as this module + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + cache_dir = os.path.join(base_dir, "cache", "gemini_descriptions") + + self.cache_dir = cache_dir + os.makedirs(cache_dir, exist_ok=True) + + def _get_file_identifier(self, file_path: str) -> str: + """Get unique identifier for a file based on path and modification time.""" + if not os.path.exists(file_path): + return f"missing:{file_path}" + + mtime = os.path.getmtime(file_path) + size = os.path.getsize(file_path) + return f"file:{file_path}:mtime:{mtime}:size:{size}" + + def _get_tensor_identifier(self, tensor_data: Any) -> str: + """Get unique identifier for tensor data by hashing its content.""" + # Convert tensor to string representation and hash it + tensor_str = str(tensor_data) + hash_obj = hashlib.sha256(tensor_str.encode('utf-8')) + return f"tensor:{hash_obj.hexdigest()[:16]}" + + def _get_cache_key(self, media_identifier: str, gemini_model: str, + model_type: str = "", options: Dict[str, Any] = None) -> str: + """Generate cache key from media identifier and configurable option settings.""" + # Hash the options dict for scalable cache keys + if options is None: + options = {} + + # Sort keys to ensure deterministic hashing regardless of dict order + options_hash = hashlib.md5(json.dumps(options, sort_keys=True).encode()).hexdigest()[:8] + + key_components = [ + media_identifier, + gemini_model, + model_type, + f"options:{options_hash}" + ] + key_string = "|".join(key_components) + hash_obj = hashlib.sha256(key_string.encode('utf-8')) + return hash_obj.hexdigest() + + def _get_cache_file_path(self, cache_key: str) -> str: + """Get the file path for storing cache entry.""" + return os.path.join(self.cache_dir, f"{cache_key}.json") + + def get(self, media_identifier: str, gemini_model: str, + model_type: str = "", options: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + Retrieve cached description if available. + + Args: + media_identifier: Unique identifier for the media + gemini_model: The Gemini model being used + model_type: The model type (e.g., "Text2Image", "ImageEdit") + options: Dictionary of configurable options (e.g., describe_clothing, describe_hair_style, etc.) + + Returns: + Cached result dictionary or None if not found + """ + cache_key = self._get_cache_key(media_identifier, gemini_model, model_type, options) + cache_file = self._get_cache_file_path(cache_key) + + if not os.path.exists(cache_file): + return None + + try: + with open(cache_file, 'r', encoding='utf-8') as f: + cached_data = json.load(f) + + # Verify cache entry has required fields + if not all(key in cached_data for key in ['description', 'timestamp', 'cache_key']): + return None + + return cached_data + + except (json.JSONDecodeError, IOError) as e: + # If cache file is corrupted, remove it + print(f"[CACHE] Corrupted cache file {cache_file}, removing: {e}") + try: + os.remove(cache_file) + except OSError: + pass + return None + + def set(self, media_identifier: str, gemini_model: str, description: str, + model_type: str = "", options: Dict[str, Any] = None, + extra_data: Optional[Dict[str, Any]] = None) -> None: + """ + Store description in cache. + + Args: + media_identifier: Unique identifier for the media + gemini_model: The Gemini model being used + description: The generated description text + model_type: The model type (e.g., "Text2Image", "ImageEdit") + options: Dictionary of configurable options (e.g., describe_clothing, describe_hair_style, etc.) + extra_data: Additional data to store (e.g., status, video_info) + """ + cache_key = self._get_cache_key(media_identifier, gemini_model, model_type, options) + cache_file = self._get_cache_file_path(cache_key) + + if options is None: + options = {} + + cache_entry = { + 'cache_key': cache_key, + 'media_identifier': media_identifier, + 'gemini_model': gemini_model, + 'model_type': model_type, + 'options': options, + 'description': description, + 'timestamp': time.time(), + 'human_timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), + } + + # Add any extra data + if extra_data: + cache_entry.update(extra_data) + + try: + with open(cache_file, 'w', encoding='utf-8') as f: + json.dump(cache_entry, f, indent=2, ensure_ascii=False) + except IOError as e: + print(f"[CACHE] Failed to write cache file {cache_file}: {e}") + + def get_cache_info(self) -> Dict[str, Any]: + """Get information about the cache.""" + if not os.path.exists(self.cache_dir): + return {'cache_dir': self.cache_dir, 'entries': 0, 'total_size': 0} + + entries = 0 + total_size = 0 + + for filename in os.listdir(self.cache_dir): + if filename.endswith('.json'): + entries += 1 + file_path = os.path.join(self.cache_dir, filename) + try: + total_size += os.path.getsize(file_path) + except OSError: + pass + + return { + 'cache_dir': self.cache_dir, + 'entries': entries, + 'total_size': total_size, + 'total_size_mb': round(total_size / (1024 * 1024), 2) + } + + +# Utility functions for different media types +def get_file_media_identifier(file_path: str) -> str: + """Get media identifier for a file path.""" + cache = GeminiCache() + return cache._get_file_identifier(file_path) + + +def get_tensor_media_identifier(tensor_data: Any) -> str: + """Get media identifier for tensor data.""" + cache = GeminiCache() + return cache._get_tensor_identifier(tensor_data) + + +# Global cache instance +_global_cache = None + + +def get_cache() -> GeminiCache: + """Get the global cache instance.""" + global _global_cache + if _global_cache is None: + _global_cache = GeminiCache() + return _global_cache \ No newline at end of file diff --git a/utils/helper_nodes.py b/utils/helper_nodes.py new file mode 100644 index 0000000..cdf6bf9 --- /dev/null +++ b/utils/helper_nodes.py @@ -0,0 +1,48 @@ +class ShowText: + """ + A simple ComfyUI custom node that displays text input. + Useful for showing the final_string output from Gemini nodes. + """ + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + """ + Return a dictionary which contains config for all input fields. + Takes a STRING input and displays it. + """ + return { + "required": { + "text": ("STRING", { + "multiline": True, + "default": "", + "tooltip": "Text to display" + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "show_text" + CATEGORY = "Utils" + OUTPUT_NODE = True # This makes it an output node that shows in the UI + + def show_text(self, text): + """ + Simply return the input text and display it + """ + print(f"ShowText node displaying: {text[:100]}...") # Log first 100 chars + return (text,) + + +# A dictionary that contains the helper nodes +HELPER_NODE_CLASS_MAPPINGS = { + "ShowText": ShowText +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +HELPER_NODE_DISPLAY_NAME_MAPPINGS = { + "ShowText": "Show Text" +} diff --git a/utils/nodes.py b/utils/nodes.py new file mode 100644 index 0000000..c53d8ad --- /dev/null +++ b/utils/nodes.py @@ -0,0 +1,1091 @@ +from google import genai +from google.genai import types +import cv2 +import tempfile +import os +import subprocess +import numpy as np +from PIL import Image +import io +from datetime import datetime +from .cache import get_cache, get_file_media_identifier, get_tensor_media_identifier + + +class GeminiUtilOptions: + """ + A ComfyUI custom node that provides configuration options for Gemini nodes. + This node outputs an options object that can be connected to Gemini processing nodes. + """ + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + """ + Return a dictionary which contains config for all input fields. + """ + return { + "required": { + "gemini_api_key": ("STRING", { + "multiline": False, + "default": "YOUR_GEMINI_API_KEY_HERE", + "tooltip": "Your Gemini API key" + }), + "gemini_model": (["models/gemini-2.5-flash", "models/gemini-2.5-flash-lite", "models/gemini-2.5-pro"], { + "default": "models/gemini-2.5-flash", + "tooltip": "Select the Gemini model to use" + }), + "model_type": (["Text2Image", "ImageEdit"], { + "default": "Text2Image", + "tooltip": "Select the type of model workflow to use (only applies to images)" + }), + "describe_clothing": (["Yes", "No"], { + "default": "No", + "tooltip": "Whether to include detailed clothing and accessory descriptions" + }), + "describe_hair_style": (["Yes", "No"], { + "default": "Yes", + "tooltip": "Whether to include hair style descriptions (texture and motion, but not color or length)" + }), + "describe_bokeh": (["Yes", "No"], { + "default": "Yes", + "tooltip": "Whether to include depth of field effects, bokeh, and blur descriptions" + }), + "describe_subject": (["Yes", "No"], { + "default": "Yes", + "tooltip": "Whether to include subject/person descriptions in the first paragraph" + }), + "prefix_text": ("STRING", { + "multiline": True, + "default": "", + "tooltip": "Text to prepend to the generated description" + }), + } + } + + RETURN_TYPES = ("GEMINI_OPTIONS",) + RETURN_NAMES = ("gemini_options",) + FUNCTION = "create_options" + CATEGORY = "Gemini" + + def create_options(self, gemini_api_key, gemini_model, model_type, describe_clothing, describe_hair_style, describe_bokeh, describe_subject, prefix_text): + """ + Create an options object with all the configuration settings + """ + options = { + "gemini_api_key": gemini_api_key, + "gemini_model": gemini_model, + "model_type": model_type, + "describe_clothing": describe_clothing == "Yes", + "describe_hair_style": describe_hair_style == "Yes", + "describe_bokeh": describe_bokeh == "Yes", + "describe_subject": describe_subject == "Yes", + "prefix_text": prefix_text + } + return (options,) + +class GeminiMediaDescribe: + """ + A ComfyUI custom node for describing images or videos using Google's Gemini API. + Supports both uploaded media and random selection from a directory path (including subdirectories). + """ + + def __init__(self): + pass + + def _ordinal(self, n): + """Convert number to ordinal string (1->First, 2->Second, etc.)""" + ordinals = ["", "First", "Second", "Third", "Fourth", "Fifth", "Sixth"] + if n < len(ordinals): + return ordinals[n] + else: + return f"{n}th" + + def _trim_video(self, input_path, output_path, duration): + """ + Trim video to specified duration from the beginning using ffmpeg + + Args: + input_path: Path to input video file + output_path: Path to output trimmed video file + duration: Duration in seconds from the beginning + """ + + try: + # Use ffmpeg to trim the video from the beginning + cmd = [ + 'ffmpeg', + '-i', input_path, + '-t', str(duration), # Duration from start + '-c', 'copy', # Copy streams without re-encoding for speed + '-avoid_negative_ts', 'make_zero', + '-y', # Overwrite output file if it exists + output_path + ] + + subprocess.run(cmd, capture_output=True, text=True, check=True) + return True + + except subprocess.CalledProcessError as e: + print(f"FFmpeg error: {e.stderr}") + # Fallback: try with re-encoding if copy fails + try: + cmd = [ + 'ffmpeg', + '-i', input_path, + '-t', str(duration), + '-c:v', 'libx264', + '-c:a', 'aac', + '-y', + output_path + ] + subprocess.run(cmd, capture_output=True, text=True, check=True) + return True + except subprocess.CalledProcessError as e2: + print(f"FFmpeg re-encoding also failed: {e2.stderr}") + return False + except FileNotFoundError: + print("FFmpeg not found. Please install ffmpeg to use duration trimming.") + return False + + def _process_image(self, gemini_api_key, gemini_model, model_type, describe_clothing, describe_hair_style, describe_bokeh, describe_subject, prefix_text, image, selected_media_path, media_info_text): + """ + Process image using logic from GeminiImageDescribe + """ + try: + # Build system prompt based on individual options + if model_type == "Text2Image": + # Initialize prompt components + prompts = [] + paragraph_num = 1 + + # Subject paragraph (conditional) + if describe_subject: + subject_prompt = f"""SUBJECT ({self._ordinal(paragraph_num)} Paragraph) +Begin with a gendered noun phrase (e.g., "A woman…", "A man…").""" + + # Add hair style description if enabled + if describe_hair_style: + subject_prompt += """ +Include hairstyle and its texture or motion (no color or length).""" + + subject_prompt += """ +Include posture, gestures as applicable. +Strictly exclude any reference to ethnicity, age, body type, tattoos, glasses, hair color, hair length, eye color, height, or makeup.""" + + prompts.append(subject_prompt) + paragraph_num += 1 + + # Build cinematic aesthetic paragraph + if describe_bokeh: + cinematic_prompt = f"""CINEMATIC AESTHETIC CONTROL ({self._ordinal(paragraph_num)} Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), optics (lens feel, DOF, rack focus), and exposure/render cues as applicable.""" + else: + cinematic_prompt = f"""CINEMATIC AESTHETIC CONTROL ({self._ordinal(paragraph_num)} Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), and exposure/render cues as applicable. Everything must be in sharp focus with no depth of field effects, bokeh, or blur. Do not mention optics, DOF, rack focus, or any depth-related visual effects.""" + + prompts.append(cinematic_prompt) + paragraph_num += 1 + + # Style paragraph + style_prompt = f"""STYLIZATION & TONE ({self._ordinal(paragraph_num)} Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.).""" + + prompts.append(style_prompt) + paragraph_num += 1 + + # Clothing paragraph (conditional) + clothing_prompt = "" + if describe_clothing: + clothing_prompt = f""" +CLOTHING ({self._ordinal(paragraph_num)} Paragraph) +Describe all visible clothing and accessories with absolute certainty and definitiveness. Be specific: identify garment type with confidence, state definitive color(s), material/texture, fit/silhouette, length, notable construction (seams, straps, waistbands), and condition. Include footwear if visible and describe exactly how fabrics respond to motion (stretching, swaying, tightening, wrinkling). Make decisive choices when multiple interpretations are possible - choose one specific description and state it as fact. Do not describe any text, typography, words, letters, logos, brand names, or written content visible on clothing or accessories. Exclude tattoos, glasses, and other prohibited attributes.""" + paragraph_num += 1 + + paragraph_count = paragraph_num - 1 + + # Build critical note based on what's included + critical_note = f"CRITICAL: Output exactly {paragraph_count} paragraphs, one per category, separated by a blank line." + + if not describe_clothing: + critical_note += " DO NOT describe clothing, accessories, or garments in any paragraph." + if not describe_subject: + critical_note += " DO NOT describe people, subjects, or human figures in any paragraph." + + # Add bokeh restriction if needed + if not describe_bokeh: + critical_note += " Never mention depth of field, bokeh, blur, optics, DOF, rack focus, or any depth-related visual effects." + + critical_note += " Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact." + + # Combine all parts + combined_prompts = "\n\n".join(prompts) + + system_prompt = f"""Generate a Wan 2.2 optimized text to image prompt. You are an expert assistant specialized in analyzing and verbalizing input media for instagram-quality posts using the Wan 2.2 Text to Image workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review the provided media. Do not use meta phrases (e.g., "this picture shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +{combined_prompts}{clothing_prompt} + +{critical_note}""" + user_prompt = f"Please analyze this image and provide a detailed description following the {paragraph_count}-paragraph structure outlined in the system prompt." + + else: # model_type == "ImageEdit" + # Build ImageEdit system prompt based on options + focus_instruction = "f/11 for deep focus—no bokeh or blur" if not describe_bokeh else "" + if not describe_bokeh: + focus_safeguards = ", no depth of field, no bokeh" + else: + focus_safeguards = "" + + # Build traits list based on options + traits_list = [] + if describe_hair_style: + traits_list.append("hairstyle and") + if describe_clothing: + traits_list.append("outfit style") + + if describe_subject: + traits_list.append("pose, posture") + traits_instruction = ", ".join(traits_list) if traits_list else "pose and posture only" + + if describe_clothing: + clothing_note = f"describe {traits_instruction} (without age, ethnicity, tattoos, hair color, etc.)" + else: + clothing_note = f"describe {traits_instruction} only (avoid clothing, age, ethnicity, tattoos, hair color, etc.)" + + system_prompt = f"""You are an expert assistant generating concise, single-sentence Qwen-Image-Edit instructions. Always be completely decisive and definitive - when you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". + +Always begin with "Make this person…", include vivid, focused scene details (e.g. bedroom props, lights, furniture or gym bench, textured wall, window views) early to anchor the setting{"," if focus_instruction else ""} {focus_instruction}, {clothing_note}, include clear torso and head orientation (e.g., "back facing the camera with torso turned 45° and head looking over her shoulder toward viewer"), reference cinematic aesthetic cues (lighting, framing, lens, shot type), anchor realism by stating skin shows subtle pores, light wrinkles, and realistic surface detail, end with "keep everything else unchanged," and include negative safeguards like "no distortion, no blur artifacts{focus_safeguards}.\"""" + else: + # No subject description - focus on environment/scene only + traits_instruction = ", ".join(traits_list) if traits_list else "environment details only" + + system_prompt = f"""You are an expert assistant generating concise, single-sentence Qwen-Image-Edit instructions. Always be completely decisive and definitive - when you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". + +Focus on vivid, focused scene details (e.g. bedroom props, lights, furniture or gym bench, textured wall, window views) to anchor the setting{"," if focus_instruction else ""} {focus_instruction}, do not describe people or human subjects, reference cinematic aesthetic cues (lighting, framing, lens, shot type), end with "keep everything else unchanged," and include negative safeguards like "no distortion, no blur artifacts{focus_safeguards}.\"""" + + user_prompt = "Please analyze this image and generate a single-sentence Qwen-Image-Edit instruction following the guidelines in the system prompt." + + # Convert image to bytes for Gemini + if image is not None: + # Convert ComfyUI IMAGE tensor to image data + if hasattr(image, 'cpu'): + image_np = image.cpu().numpy() + else: + image_np = image + + # Take the first image from the batch if multiple images + if len(image_np.shape) == 4: + image_array = image_np[0] + else: + image_array = image_np + + # Convert from 0-1 float to 0-255 uint8 + if image_array.dtype == np.float32 or image_array.dtype == np.float64: + image_array = (image_array * 255).astype(np.uint8) + + # Convert numpy array to PIL Image + if len(image_array.shape) == 3 and image_array.shape[2] == 3: + pil_image = Image.fromarray(image_array, 'RGB') + elif len(image_array.shape) == 3 and image_array.shape[2] == 4: + pil_image = Image.fromarray(image_array, 'RGBA') + else: + pil_image = Image.fromarray(image_array).convert('RGB') + + # Convert PIL image to bytes + img_byte_arr = io.BytesIO() + pil_image.save(img_byte_arr, format='JPEG') + image_data = img_byte_arr.getvalue() + + # Update media info + media_info_text += f"\n• Resolution: {pil_image.size[0]}x{pil_image.size[1]}" + elif selected_media_path: + # Read image from file path + pil_image = Image.open(selected_media_path) + if pil_image.mode != 'RGB': + pil_image = pil_image.convert('RGB') + + # Convert PIL image to bytes + img_byte_arr = io.BytesIO() + pil_image.save(img_byte_arr, format='JPEG') + image_data = img_byte_arr.getvalue() + + # Update media info + file_size = os.path.getsize(selected_media_path) / 1024 / 1024 # Size in MB + media_info_text += f"\n• Resolution: {pil_image.size[0]}x{pil_image.size[1]}\n• File Size: {file_size:.2f} MB" + else: + raise ValueError("No image data available for processing") + + # Determine media identifier for caching + if selected_media_path: + # For file-based media, use file path + modification time + media_identifier = get_file_media_identifier(selected_media_path) + else: + # For tensor-based media, use content hash + media_identifier = get_tensor_media_identifier(image) + + # Check cache for existing result + cache = get_cache() + + # Build options dict for caching + cache_options = { + "describe_clothing": describe_clothing, + "describe_hair_style": describe_hair_style, + "describe_bokeh": describe_bokeh, + "describe_subject": describe_subject + } + + cached_result = cache.get( + media_identifier=media_identifier, + gemini_model=gemini_model, + model_type=model_type, + options=cache_options + ) + + if cached_result is not None: + # Return cached result + description = cached_result['description'] + + # Format outputs for cached image processing + gemini_status = f"""🤖 Gemini Analysis Status: ✅ Complete (Cached) +• Model: {gemini_model} +• Model Type: {model_type} +• API Key: {'*' * (len(gemini_api_key) - 4) + gemini_api_key[-4:] if len(gemini_api_key) >= 4 else '****'} +• Input: Image +• Cache: HIT at {cached_result.get('human_timestamp', 'unknown time')}""" + + processed_media_path = selected_media_path if selected_media_path else "" + final_string = f"{prefix_text}{description}" if prefix_text else description + + return (description, media_info_text, gemini_status, processed_media_path, final_string) + + # Initialize the Gemini client + client = genai.Client(api_key=gemini_api_key) + + # Create the content structure for image analysis + contents = [ + types.Content( + role="user", + parts=[ + types.Part.from_bytes( + mime_type="image/jpeg", + data=image_data, + ), + ], + ), + types.Content( + role="user", + parts=[ + types.Part.from_text(text=f"{system_prompt}\n\n{user_prompt}"), + ], + ), + ] + + # Configure generation with thinking enabled + generate_content_config = types.GenerateContentConfig( + thinking_config=types.ThinkingConfig( + thinking_budget=-1, + ), + ) + + # Generate the image description + response = client.models.generate_content( + model=gemini_model, + contents=contents, + config=generate_content_config, + ) + + # Process response + if response.text is not None: + description = response.text.strip() + + # Store successful result in cache + cache.set( + media_identifier=media_identifier, + gemini_model=gemini_model, + description=description, + model_type=model_type, + options=cache_options + ) + else: + error_msg = "Error: Gemini returned empty response" + if hasattr(response, 'prompt_feedback') and response.prompt_feedback: + error_msg += f" (Prompt feedback: {response.prompt_feedback})" + if hasattr(response, 'candidates') and response.candidates: + error_msg += f" (Candidates available: {len(response.candidates)})" + # Raise exception to stop workflow execution + raise RuntimeError(error_msg) + + # Format outputs for image processing + gemini_status = f"""🤖 Gemini Analysis Status: ✅ Complete +• Model: {gemini_model} +• Model Type: {model_type} +• API Key: {'*' * (len(gemini_api_key) - 4) + gemini_api_key[-4:] if len(gemini_api_key) >= 4 else '****'} +• Input: Image""" + + processed_media_path = selected_media_path if selected_media_path else "" + final_string = f"{prefix_text}{description}" if prefix_text else description + + return (description, media_info_text, gemini_status, processed_media_path, final_string) + + except Exception as e: + # Re-raise the exception to stop workflow execution + raise Exception(f"Image analysis failed: {str(e)}") + + def _process_video(self, gemini_api_key, gemini_model, describe_clothing, describe_hair_style, describe_bokeh, describe_subject, prefix_text, selected_media_path, frame_rate, max_duration, media_info_text): + """ + Process video using logic from GeminiVideoDescribe + """ + try: + # Build system prompt based on individual options for video + # Initialize paragraph tracking + paragraph_num = 1 + prompts = [] + + # Subject paragraph (conditional) + if describe_subject: + subject_prompt = f"""{paragraph_num}. SUBJECT ({self._ordinal(paragraph_num)} Paragraph) +Begin with a gendered noun phrase (e.g., "A woman…", "A man…").""" + + # Add hair style description if enabled + if describe_hair_style: + subject_prompt += """ +Include hairstyle and its texture or motion (no color or length).""" + + subject_prompt += """ +Include posture, gestures as applicable. +Strictly exclude any reference to ethnicity, age, body type, tattoos, glasses, hair color, hair length, eye color, height, or makeup.""" + + prompts.append(subject_prompt) + paragraph_num += 1 + + # Build clothing paragraph (conditional) + if describe_clothing: + clothing_prompt = f""" +{paragraph_num}. CLOTHING ({self._ordinal(paragraph_num)} Paragraph) +Describe all visible clothing and accessories with absolute certainty and definitiveness. Be specific: identify garment type with confidence, state definitive color(s), material/texture, fit/silhouette, length, notable construction (seams, straps, waistbands), and condition. Include footwear if visible and describe exactly how fabrics respond to motion (stretching, swaying, tightening, wrinkling). Make decisive choices when multiple interpretations are possible - choose one specific description and state it as fact. Do not describe any text, typography, words, letters, logos, brand names, or written content visible on clothing or accessories. Exclude tattoos, glasses, and other prohibited attributes.""" + paragraph_num += 1 + else: + clothing_prompt = "" + + # Scene paragraph + scene_prompt = f""" +{paragraph_num}. SCENE ({self._ordinal(paragraph_num)} Paragraph) +Describe the visible environment clearly and vividly.""" + prompts.append(scene_prompt) + paragraph_num += 1 + + # Movement paragraph + movement_prompt = f""" +{paragraph_num}. MOVEMENT ({self._ordinal(paragraph_num)} Paragraph) +In this paragraph, describe body-part–specific movement and how it aligns with musical rhythm and beat structure. Begin with an overall summary: e.g., 'The subject initiates with a hip sway on the downbeat…'. Then narrate movement chronologically, using precise action verbs and transitions like 'then', 'as', and 'after', referencing the timeline (e.g., early/mid/late beat or second). Specify which body parts move, how they articulate (e.g., 'the right arm lifts upward, then sweeps outward; the torso tilts as the knees bend'), describe footwork, weight shifts, and alignment with music beats. Also include any camera movement (e.g., 'camera pans to follow the torso shift'). Avoid general labels—focus on locomotor and non‑locomotor gestures, repetition, rhythm, and choreography phrasing. Always include any buttock or breast movements that you see""" + prompts.append(movement_prompt) + paragraph_num += 1 + + # Build cinematic aesthetic paragraph + if describe_bokeh: + cinematic_prompt = f""" +{paragraph_num}. CINEMATIC AESTHETIC CONTROL ({self._ordinal(paragraph_num)} Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), optics (lens feel, DOF, rack focus), and exposure/render cues as applicable.""" + else: + cinematic_prompt = f""" +{paragraph_num}. CINEMATIC AESTHETIC CONTROL ({self._ordinal(paragraph_num)} Paragraph) +Lighting (source/direction/quality/temperature), camera details (shot type, angle/height, movement), and exposure/render cues as applicable. Everything must be in sharp focus with no depth of field effects, bokeh, or blur. Do not mention optics, DOF, rack focus, or any depth-related visual effects.""" + + prompts.append(cinematic_prompt) + paragraph_num += 1 + + # Style paragraph + style_prompt = f""" +{paragraph_num}. STYLIZATION & TONE ({self._ordinal(paragraph_num)} Paragraph) +Mood/genre descriptors (e.g., "noir-inspired silhouette," "cinematic realism," etc.).""" + prompts.append(style_prompt) + + paragraph_count = paragraph_num + + # Build critical note based on what's included + critical_note = f"CRITICAL: Output exactly {paragraph_count} paragraphs, one per category, separated by a blank line." + if not describe_clothing: + critical_note += " DO NOT describe clothing, accessories, or garments in any paragraph." + if not describe_subject: + critical_note += " DO NOT describe people, subjects, or human figures in any paragraph." + if not describe_bokeh: + critical_note += " Never mention depth of field, bokeh, blur, optics, DOF, rack focus, or any depth-related visual effects." + critical_note += " Never mention prohibited attributes, even if visible. Be completely decisive and definitive in all descriptions - eliminate all uncertainty language including 'appears to be', 'seems to be', 'might be', 'possibly', 'likely', 'or', 'either/or'. When multiple interpretations are possible, confidently choose one and state it as absolute fact." + + # Combine all parts + combined_prompts = "\n".join(prompts) + + system_prompt = f"""You are an expert assistant specialized in analyzing and verbalizing input videos for cinematic-quality video transformation using the Wan 2.2 + VACE workflow. + +DECISIVENESS REQUIREMENT: Always provide definitive, certain descriptions. When you see something that could be described multiple ways, make a confident choice and state it as fact. Never use uncertain language like "appears to be", "seems to be", "might be", "possibly", "likely", or "or". Instead of "holding a black folder or book", write "holding a black folder". Instead of "wearing what appears to be denim", write "wearing dark blue denim jeans". + +Before writing, silently review all provided frames as a single clip and infer motion across time; reason stepwise over the entire sequence (start → middle → end). Do not use meta phrases (e.g., "this video shows"). +Generate descriptions that adhere to the following structured layers and constraints, formatting each as a SEPARATE PARAGRAPH in this exact order: + +{combined_prompts}{clothing_prompt} + +{critical_note}""" + user_prompt = f"Please analyze this video and provide a detailed description following the {paragraph_count}-paragraph structure outlined in the system prompt." + + # Process video file + if not selected_media_path or not os.path.exists(selected_media_path): + raise ValueError(f"Video file not found: {selected_media_path}") + + # Get original video info using OpenCV + cap = cv2.VideoCapture(selected_media_path) + frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + fps = cap.get(cv2.CAP_PROP_FPS) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + original_duration = frame_count / fps if fps > 0 else 0 + cap.release() + + # Determine the video file to use for analysis + final_video_path = selected_media_path + actual_duration = original_duration + trimmed = False + trimmed_video_output_path = selected_media_path + + # Calculate duration based on max_duration + if max_duration > 0: + actual_duration = min(max_duration, original_duration) + + # Check if we need to trim the video (only duration limit) + if max_duration > 0 and actual_duration < original_duration: + # Create a temporary trimmed video file + with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as temp_file: + trimmed_video_path = temp_file.name + + # Attempt to trim the video + if self._trim_video(selected_media_path, trimmed_video_path, actual_duration): + final_video_path = trimmed_video_path + trimmed = True + trimmed_video_output_path = trimmed_video_path + else: + print(f"Warning: Could not trim video. Using original video for {actual_duration:.2f}s") + actual_duration = original_duration + trimmed_video_output_path = selected_media_path + + # Read the final video file (original or trimmed) + with open(final_video_path, 'rb') as video_file: + video_data = video_file.read() + + file_size = len(video_data) / 1024 / 1024 # Size in MB + + # Update video info to include trimming details + end_time = actual_duration # Since we start from 0 + trim_info = f" (trimmed: 0.0s → {end_time:.1f}s)" if trimmed else "" + + updated_media_info = f"""{media_info_text} +• Original Duration: {original_duration:.2f} seconds +• Start Time: 0.0 seconds +• End Time: {end_time:.2f} seconds +• Processed Duration: {actual_duration:.2f} seconds{trim_info} +• Frames: {frame_count} +• Frame Rate: {fps:.2f} FPS +• Resolution: {width}x{height} +• File Size: {file_size:.2f} MB""" + + # Determine media identifier for caching (always file-based for videos) + media_identifier = get_file_media_identifier(selected_media_path) + + # Check cache for existing result + cache = get_cache() + + # Build options dict for caching + cache_options = { + "describe_clothing": describe_clothing, + "describe_hair_style": describe_hair_style, + "describe_bokeh": describe_bokeh, + "describe_subject": describe_subject + } + + cached_result = cache.get( + media_identifier=media_identifier, + gemini_model=gemini_model, + model_type="", # Videos don't use model_type + options=cache_options + ) + + if cached_result is not None: + # Return cached result + description = cached_result['description'] + + # Format outputs for cached video processing + gemini_status = f"""🤖 Gemini Analysis Status: ✅ Complete (Cached) +• Model: {gemini_model} +• API Key: {'*' * (len(gemini_api_key) - 4) + gemini_api_key[-4:] if len(gemini_api_key) >= 4 else '****'} +• Input: Video +• Cache: HIT at {cached_result.get('human_timestamp', 'unknown time')}""" + + processed_media_path = selected_media_path if selected_media_path else "" + final_string = f"{prefix_text}{description}" if prefix_text else description + + return (description, updated_media_info, gemini_status, processed_media_path, final_string) + + # Initialize the Gemini client + client = genai.Client(api_key=gemini_api_key) + + # Create the content structure for video analysis + contents = [ + types.Content( + role="user", + parts=[ + types.Part.from_bytes( + mime_type="video/mp4", + data=video_data, + ), + ], + ), + types.Content( + role="user", + parts=[ + types.Part.from_text(text=f"{system_prompt}\n\n{user_prompt}"), + ], + ), + ] + + # Configure generation with thinking enabled + generate_content_config = types.GenerateContentConfig( + thinking_config=types.ThinkingConfig( + thinking_budget=-1, + ), + ) + + # Generate the video description + response = client.models.generate_content( + model=gemini_model, + contents=contents, + config=generate_content_config, + ) + + # Process response + if response.text is not None: + description = response.text.strip() + + # Store successful result in cache + cache.set( + media_identifier=media_identifier, + gemini_model=gemini_model, + description=description, + model_type="", # Videos don't use model_type + options=cache_options + ) + else: + error_msg = "Error: Gemini returned empty response" + if hasattr(response, 'prompt_feedback') and response.prompt_feedback: + error_msg += f" (Prompt feedback: {response.prompt_feedback})" + if hasattr(response, 'candidates') and response.candidates: + error_msg += f" (Candidates available: {len(response.candidates)})" + # Raise exception to stop workflow execution + raise RuntimeError(error_msg) + + # Format outputs for video processing + gemini_status = f"""🤖 Gemini Analysis Status: ✅ Complete +• Model: {gemini_model} +• API Key: {'*' * (len(gemini_api_key) - 4) + gemini_api_key[-4:] if len(gemini_api_key) >= 4 else '****'} +• Input: Video""" + + final_string = f"{prefix_text}{description}" if prefix_text else description + + return (description, updated_media_info, gemini_status, trimmed_video_output_path, final_string) + + except Exception as e: + # Re-raise the exception to stop workflow execution + raise Exception(f"Video analysis failed: {str(e)}") + + @classmethod + def INPUT_TYPES(s): + """ + Return a dictionary which contains config for all input fields. + Supports both image and video inputs with upload or random selection from path. + """ + return { + "required": { + "media_source": (["Upload Media", "Randomize Media from Path"], { + "default": "Upload Media", + "tooltip": "Choose whether to upload media or randomize from a directory path" + }), + "media_type": (["image", "video"], { + "default": "image", + "tooltip": "Select the type of media to analyze" + }), + "seed": ("INT", { + "default": 0, + "min": 0, + "max": 0xFFFFFFFFFFFFFFFF, + "tooltip": "Seed for randomization when using 'Randomize Media from Path'. Use different seeds to force re-execution." + }), + }, + "optional": { + "gemini_options": ("GEMINI_OPTIONS", { + "tooltip": "Configuration options from Gemini Util - Options node" + }), + "image": ("IMAGE", { + "tooltip": "Input image to analyze (used when media_source is Upload Media and media_type is image)" + }), + "media_path": ("STRING", { + "multiline": False, + "default": "", + "tooltip": "Directory path to randomly select media from, including all subdirectories (used when media_source is Randomize Media from Path)" + }), + "uploaded_image_file": ("STRING", { + "default": "", + "tooltip": "Path to uploaded image file (managed by upload widget)" + }), + "uploaded_video_file": ("STRING", { + "default": "", + "tooltip": "Path to uploaded video file (managed by upload widget)" + }), + "frame_rate": ("FLOAT", { + "default": 30, + "min": 1.0, + "max": 60.0, + "step": 0.1, + "tooltip": "Frame rate for the temporary video file (used when processing VIDEO input)" + }), + "max_duration": ("FLOAT", { + "default": 5.0, + "min": 0.0, + "max": 300.0, + "step": 0.1, + "tooltip": "Maximum duration in seconds (0 = use full video, only applies to videos)" + }), + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("description", "media_info", "gemini_status", "processed_media_path", "final_string") + FUNCTION = "describe_media" + CATEGORY = "Gemini" + + def describe_media(self, media_source, media_type, seed, gemini_options=None, image=None, media_path="", uploaded_image_file="", uploaded_video_file="", frame_rate=24.0, max_duration=0.0): + """ + Process media (image or video) and analyze with Gemini + + Args: + media_source: Source of media ("Upload Media" or "Randomize Media from Path") + media_type: Type of media ("image" or "video") + seed: Seed for randomization when using 'Randomize Media from Path'. Use different seeds to force re-execution. + gemini_options: Configuration options from Gemini Util - Options node (optional) + image: ComfyUI IMAGE tensor (optional, used for uploaded images) + media_path: Directory path to randomly select media from, including subdirectories (optional) + uploaded_image_file: Path to uploaded image file (optional) + uploaded_video_file: Path to uploaded video file (optional) + frame_rate: Frame rate for temporary video (legacy parameter, not used) + max_duration: Maximum duration in seconds (0 = use full video, only applies to videos) + """ + # Initialize variables that might be needed in exception handler + selected_media_path = None + media_info_text = "" + + # Handle missing gemini_options with defaults + if gemini_options is None: + gemini_options = { + "gemini_api_key": "YOUR_GEMINI_API_KEY_HERE", + "gemini_model": "models/gemini-2.5-flash", + "model_type": "Text2Image", + "describe_clothing": False, + "describe_hair_style": True, + "describe_bokeh": True, + "describe_subject": True, + "prefix_text": "" + } + + # Extract values from options + gemini_api_key = gemini_options["gemini_api_key"] + gemini_model = gemini_options["gemini_model"] + model_type = gemini_options["model_type"] + describe_clothing = gemini_options["describe_clothing"] + describe_hair_style = gemini_options["describe_hair_style"] + describe_bokeh = gemini_options["describe_bokeh"] + describe_subject = gemini_options["describe_subject"] + prefix_text = gemini_options["prefix_text"] + + try: + # Import required modules + import os + import random + import glob + + # First, determine what media we're processing + + if media_source == "Randomize Media from Path": + if not media_path or not media_path.strip(): + raise ValueError("Media path is required when using 'Randomize Media from Path'") + + # Validate path exists + if not os.path.exists(media_path): + current_dir = os.getcwd() + parent_dir = os.path.dirname(media_path) if media_path else "N/A" + parent_exists = os.path.exists(parent_dir) if parent_dir else False + + debug_info = f""" +Path Debug Info: +• Requested path: {media_path} +• Current working dir: {current_dir} +• Parent directory: {parent_dir} +• Parent exists: {parent_exists} +• Is absolute path: {os.path.isabs(media_path) if media_path else False}""" + + raise ValueError(f"Media path does not exist: {media_path}{debug_info}") + + # Define supported file extensions + if media_type == "image": + extensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.tiff", "*.webp"] + else: # video + extensions = ["*.mp4", "*.avi", "*.mov", "*.mkv", "*.wmv", "*.flv", "*.webm"] + + # Find all matching files (including subdirectories) + all_files = [] + for ext in extensions: + # Search in root directory + all_files.extend(glob.glob(os.path.join(media_path, ext))) + all_files.extend(glob.glob(os.path.join(media_path, ext.upper()))) + # Search in subdirectories recursively + all_files.extend(glob.glob(os.path.join(media_path, "**", ext), recursive=True)) + all_files.extend(glob.glob(os.path.join(media_path, "**", ext.upper()), recursive=True)) + + if not all_files: + try: + dir_contents = os.listdir(media_path) + total_files = len(dir_contents) + sample_files = dir_contents[:5] # Show first 5 files + + debug_info = f""" +Directory scan results: +• Path: {media_path} +• Total items in directory: {total_files} +• Sample files: {sample_files} +• Looking for {media_type} files with extensions: {extensions} +• Search includes subdirectories recursively""" + + raise ValueError(f"No {media_type} files found in path: {media_path}{debug_info}") + except PermissionError: + raise ValueError(f"Permission denied accessing path: {media_path}") + except Exception as scan_error: + raise ValueError(f"Error scanning path {media_path}: {str(scan_error)}") + + # Randomly select a file using the seed for reproducible selection + # When seed changes, a different file may be selected, forcing re-execution + random.seed(seed) + selected_media_path = random.choice(all_files) + + # Reset random state to avoid affecting other operations + random.seed(None) + + if media_type == "image": + # For random image, we'll read it as PIL and convert to bytes + media_info_text = f"📷 Image Processing Info (Random Selection):\n• File: {os.path.basename(selected_media_path)}\n• Source: Random from {media_path} (including subdirectories)\n• Full path: {selected_media_path}" + else: + # For random video, set up for video processing + media_info_text = f"📹 Video Processing Info (Random Selection):\n• File: {os.path.basename(selected_media_path)}\n• Source: Random from {media_path} (including subdirectories)\n• Full path: {selected_media_path}" + else: + # Upload Media mode + if media_type == "image": + if image is None and not uploaded_image_file: + raise ValueError("Image input is required when media_source is 'Upload Media' and media_type is 'image'") + if uploaded_image_file: + # Use uploaded image file + try: + import folder_paths + input_dir = folder_paths.get_input_directory() + except ImportError: + input_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "input") + selected_media_path = os.path.join(input_dir, uploaded_image_file) + media_info_text = f"📷 Image Processing Info (Uploaded File):\n• File: {uploaded_image_file}" + else: + # Use image tensor input + media_info_text = "📷 Image Processing Info (Tensor Input):\n• Source: ComfyUI IMAGE tensor" + else: # video + if not uploaded_video_file: + raise ValueError("Video upload is required when media_source is 'Upload Media' and media_type is 'video'") + try: + import folder_paths + input_dir = folder_paths.get_input_directory() + except ImportError: + input_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "input") + selected_media_path = os.path.join(input_dir, uploaded_video_file) + media_info_text = f"📹 Video Processing Info (Uploaded File):\n• File: {uploaded_video_file}" + + # Now process the media based on type + if media_type == "image": + # Process as image - delegate to image logic + return self._process_image( + gemini_api_key, gemini_model, model_type, describe_clothing, describe_hair_style, describe_bokeh, describe_subject, prefix_text, + image, selected_media_path, media_info_text + ) + else: + # Process as video - delegate to video logic + return self._process_video( + gemini_api_key, gemini_model, describe_clothing, describe_hair_style, describe_bokeh, describe_subject, prefix_text, + selected_media_path, frame_rate, max_duration, media_info_text + ) + + except Exception as e: + # Re-raise the exception to stop workflow execution + raise Exception(f"Media analysis failed: {str(e)}") + + +class FilenameGenerator: + """ + A ComfyUI custom node that generates structured filenames based on workflow parameters. + Creates filenames with optional date subdirectory and all parameter values included. + """ + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + """ + Return a dictionary which contains config for all input fields. + """ + return { + "required": { + "scheduler": ("STRING", { + "forceInput": True, + "tooltip": "Scheduler input from WanVideo Scheduler Selector or other scheduler nodes" + }), + "shift": ("FLOAT", { + "default": 12.0, + "min": 0.0, + "max": 100.0, + "step": 0.01, + "tooltip": "Shift value" + }), + "total_steps": ("INT", { + "default": 40, + "min": 1, + "max": 10000, + "tooltip": "Total number of steps" + }), + "shift_step": ("INT", { + "default": 25, + "min": 1, + "max": 10000, + "tooltip": "Shift step value" + }), + "high_cfg": ("FLOAT", { + "default": 3.0, + "min": 0.0, + "max": 30.0, + "step": 0.01, + "tooltip": "High CFG value" + }), + "low_cfg": ("FLOAT", { + "default": 4.0, + "min": 0.0, + "max": 30.0, + "step": 0.01, + "tooltip": "Low CFG value" + }), + "base_filename": ("STRING", { + "default": "base", + "tooltip": "Base filename (without extension)" + }), + "subdirectory_prefix": ("STRING", { + "default": "", + "tooltip": "Optional subdirectory prefix (e.g., 'project_name'). Will be added before date subdirectory." + }), + "add_date_subdirectory": ("BOOLEAN", { + "default": True, + "tooltip": "Add date subdirectory (YYYY-MM-DD format)" + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("filename",) + FUNCTION = "generate_filename" + CATEGORY = "Utils" + + def generate_filename(self, scheduler, shift, total_steps, shift_step, high_cfg, low_cfg, base_filename, subdirectory_prefix, add_date_subdirectory): + """ + Generate a structured filename based on the provided parameters. + + Args: + scheduler: Scheduler string from scheduler node + shift: Shift value + total_steps: Total number of steps + shift_step: Shift step value + high_cfg: High CFG value + low_cfg: Low CFG value + base_filename: Base filename + subdirectory_prefix: Optional subdirectory prefix + add_date_subdirectory: Whether to add date subdirectory + + Returns: + Generated filename string + """ + try: + # Format float values to replace decimal points with underscores + shift_str = f"{shift:.2f}".replace(".", "_") + high_cfg_str = f"{high_cfg:.2f}".replace(".", "_") + low_cfg_str = f"{low_cfg:.2f}".replace(".", "_") + + # Clean scheduler string to ensure it's filename-safe + scheduler_clean = str(scheduler).strip().replace(" ", "_").lower() + + # Clean base filename to ensure it's filename-safe + base_clean = base_filename.strip().replace(" ", "_") + + # Generate the filename components + filename_parts = [ + base_clean, + "scheduler", scheduler_clean, + "shift", shift_str, + "total_steps", str(total_steps), + "shift_step", str(shift_step), + "highCFG", high_cfg_str, + "lowCFG", low_cfg_str + ] + + # Join all parts with underscores + filename = "_".join(filename_parts) + + # Build directory structure with optional subdirectory prefix and date + directory_parts = [] + + # Add subdirectory prefix if provided + if subdirectory_prefix and subdirectory_prefix.strip(): + prefix_clean = subdirectory_prefix.strip().replace(" ", "_") + directory_parts.append(prefix_clean) + + # Add date subdirectory if requested + if add_date_subdirectory: + current_date = datetime.now().strftime("%Y-%m-%d") + directory_parts.append(current_date) + + # Combine directory parts with filename + if directory_parts: + full_path = "/".join(directory_parts) + "/" + filename + else: + full_path = filename + + return (full_path,) + + except Exception as e: + raise Exception(f"Filename generation failed: {str(e)}") + + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "GeminiUtilMediaDescribe": GeminiMediaDescribe, + "GeminiUtilOptions": GeminiUtilOptions, + "FilenameGenerator": FilenameGenerator +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "GeminiUtilMediaDescribe": "Gemini Util - Media Describe", + "GeminiUtilOptions": "Gemini Util - Options", + "FilenameGenerator": "Filename Generator" +} diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000..2a18d1c --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 4, + "useTabs": false +} \ No newline at end of file diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..eb8bc9f --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,25 @@ +import js from "@eslint/js"; + +export default [ + js.configs.recommended, + { + files: ["js/**/*.js"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + console: "readonly", + window: "readonly", + document: "readonly", + app: "readonly", + api: "readonly" + } + }, + rules: { + "no-unused-vars": "warn", + "no-console": "off", + "prefer-const": "error", + "no-var": "error" + } + } +]; \ No newline at end of file diff --git a/web/js/gemini_widgets.js b/web/js/gemini_widgets.js new file mode 100644 index 0000000..9ca2bd9 --- /dev/null +++ b/web/js/gemini_widgets.js @@ -0,0 +1,1114 @@ +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; + +console.log("Loading gemini_widgets.js extension"); + +// Register custom widget for the Gemini Video Describe node +app.registerExtension({ + name: "comfyui_swissarmyknife.gemini_widgets", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // Handle GeminiUtilOptions node + if (nodeData.name === "GeminiUtilOptions") { + console.log("Registering GeminiUtilOptions node"); + + // This node doesn't need special widgets - it just provides configuration + // The existing ComfyUI widgets are sufficient for this node + } + + // Handle FilenameGenerator node + else if (nodeData.name === "FilenameGenerator") { + console.log("Registering FilenameGenerator node"); + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const result = onNodeCreated?.apply(this, arguments); + + // Add a preview widget to show the generated filename + this.addWidget( + "text", + "filename_preview", + "", + (value) => { + // This is a read-only preview widget + }, + { multiline: true } + ); + + // Function to update the filename preview + this.updateFilenamePreview = function () { + try { + // For scheduler, check if it's connected via input first + let scheduler = "scheduler_input"; + const schedulerInput = this.inputs?.find( + (input) => input.name === "scheduler" + ); + if (schedulerInput && schedulerInput.link) { + scheduler = "scheduler_connected"; + } else { + // Fallback to widget if no connection (shouldn't happen with SAMPLER type, but for safety) + const schedulerWidget = this.widgets.find( + (w) => w.name === "scheduler" + ); + if (schedulerWidget) { + scheduler = schedulerWidget.value || "scheduler_input"; + } + } + + const shift = this.widgets.find((w) => w.name === "shift")?.value || 12.0; + const total_steps = + this.widgets.find((w) => w.name === "total_steps")?.value || 40; + const shift_step = + this.widgets.find((w) => w.name === "shift_step")?.value || 25; + const high_cfg = + this.widgets.find((w) => w.name === "high_cfg")?.value || 3.0; + const low_cfg = + this.widgets.find((w) => w.name === "low_cfg")?.value || 4.0; + const base_filename = + this.widgets.find((w) => w.name === "base_filename")?.value || "base"; + const subdirectory_prefix = + this.widgets.find((w) => w.name === "subdirectory_prefix")?.value || ""; + const add_date_subdirectory = + this.widgets.find((w) => w.name === "add_date_subdirectory")?.value !== + false; + + // Format float values to replace decimal points with underscores + const shift_str = shift.toFixed(2).replace(".", "_"); + const high_cfg_str = high_cfg.toFixed(2).replace(".", "_"); + const low_cfg_str = low_cfg.toFixed(2).replace(".", "_"); + + // Clean scheduler string to ensure it's filename-safe + const scheduler_clean = scheduler + .toString() + .trim() + .replace(/\s/g, "_") + .toLowerCase(); + + // Clean base filename to ensure it's filename-safe + const base_clean = base_filename.toString().trim().replace(/\s/g, "_"); + + // Generate the filename components + const filename_parts = [ + base_clean, + "scheduler", + scheduler_clean, + "shift", + shift_str, + "total_steps", + total_steps.toString(), + "shift_step", + shift_step.toString(), + "highCFG", + high_cfg_str, + "lowCFG", + low_cfg_str, + ]; + + // Join all parts with underscores + let filename = filename_parts.join("_"); + + // Build directory structure with optional subdirectory prefix and date + const directory_parts = []; + + // Add subdirectory prefix if provided + if (subdirectory_prefix && subdirectory_prefix.trim()) { + const prefix_clean = subdirectory_prefix.trim().replace(/\s/g, "_"); + directory_parts.push(prefix_clean); + } + + // Add date subdirectory if requested + if (add_date_subdirectory) { + const current_date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format + directory_parts.push(current_date); + } + + // Combine directory parts with filename + let full_path; + if (directory_parts.length > 0) { + full_path = directory_parts.join("/") + "/" + filename; + } else { + full_path = filename; + } + + // Update the preview widget + const previewWidget = this.widgets.find( + (w) => w.name === "filename_preview" + ); + if (previewWidget) { + previewWidget.value = `Preview:\n${full_path}`; + } + } catch (error) { + console.log("Error updating filename preview:", error); + const previewWidget = this.widgets.find( + (w) => w.name === "filename_preview" + ); + if (previewWidget) { + previewWidget.value = "Preview:\nError generating filename"; + } + } + }; + + // Set up listeners for all input widgets to update the preview + const inputWidgetNames = [ + "shift", + "total_steps", + "shift_step", + "high_cfg", + "low_cfg", + "base_filename", + "subdirectory_prefix", + "add_date_subdirectory", + ]; + + for (const widgetName of inputWidgetNames) { + const widget = this.widgets.find((w) => w.name === widgetName); + if (widget) { + const originalCallback = widget.callback; + widget.callback = (...args) => { + if (originalCallback) { + originalCallback.apply(widget, args); + } + // Update preview after a small delay to ensure the value is set + setTimeout(() => this.updateFilenamePreview(), 10); + }; + } + } + + // Listen for input connections/disconnections to update preview + const originalOnConnectionsChange = this.onConnectionsChange; + this.onConnectionsChange = function (type, slotIndex, connected, linkInfo, ioSlot) { + if (originalOnConnectionsChange) { + originalOnConnectionsChange.call( + this, + type, + slotIndex, + connected, + linkInfo, + ioSlot + ); + } + // Update preview when scheduler input changes + if (type === 1 && ioSlot && ioSlot.name === "scheduler") { + // type 1 = input + setTimeout(() => this.updateFilenamePreview(), 10); + } + }; + + // Initial preview update + setTimeout(() => this.updateFilenamePreview(), 100); + + return result; + }; + } + + // Handle GeminiUtilMediaDescribe node + else if (nodeData.name === "GeminiUtilMediaDescribe") { + console.log("Registering GeminiUtilMediaDescribe node with dynamic media widgets"); + + // Add custom widget after the node is created + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const result = onNodeCreated?.apply(this, arguments); + + // Hide the optional input widgets that shouldn't be directly visible + // These will be managed by our dynamic widget system + this.hideOptionalInputWidgets = function () { + const widgetsToHide = [ + "media_path", + "uploaded_image_file", + "uploaded_video_file", + "seed", + ]; + + for (const widgetName of widgetsToHide) { + const widget = this.widgets.find((w) => w.name === widgetName); + if (widget) { + // Hide the widget by setting its type to 'hidden' + widget.type = "hidden"; + widget.computeSize = () => [0, -4]; // Make it take no space + console.log(`[WIDGET] Hidden optional input widget: ${widgetName}`); + } + } + }; + + // Hide the optional input widgets immediately + this.hideOptionalInputWidgets(); + + // Find the media_source widget + this.mediaSourceWidget = this.widgets.find((w) => w.name === "media_source"); + + // Find the media_type widget + this.mediaTypeWidget = this.widgets.find((w) => w.name === "media_type"); + + // Method to clear all media state (images, videos, previews, file data) + this.clearAllMediaState = function () { + console.log("[DEBUG] clearAllMediaState called"); + console.log("[DEBUG] _pendingFileRestore exists:", !!this._pendingFileRestore); + + // Clear video state and preview + this.clearVideoPreview(); + this.uploadedVideoFile = null; + this.uploadedVideoSubfolder = null; + + // Clear image state + this.uploadedImageFile = null; + this.uploadedImageSubfolder = null; + + // Reset widget values to defaults (only upload-related widgets) + // FIXED: Only clear if widgets exist and we're not in restoration mode + if (this.videoInfoWidget && !this._pendingFileRestore) { + console.log("[DEBUG] Clearing videoInfoWidget"); + this.videoInfoWidget.value = "No video selected"; + } else if (this.videoInfoWidget && this._pendingFileRestore) { + console.log( + "[DEBUG] Skipping videoInfoWidget clear due to pending restore" + ); + } + + if (this.imageInfoWidget && !this._pendingFileRestore) { + console.log("[DEBUG] Clearing imageInfoWidget"); + this.imageInfoWidget.value = "No image selected"; + } else if (this.imageInfoWidget && this._pendingFileRestore) { + console.log( + "[DEBUG] Skipping imageInfoWidget clear due to pending restore" + ); + } + + // Don't clear media_path as it's not related to upload state + // if (this.mediaPathWidget) { + // this.mediaPathWidget.value = ""; + // } + + // Clear hidden widgets that store file paths for Python node + if (this.videoFileWidget && !this._pendingFileRestore) { + console.log("[DEBUG] Clearing videoFileWidget"); + this.videoFileWidget.value = ""; + } else if (this.videoFileWidget && this._pendingFileRestore) { + console.log( + "[DEBUG] Skipping videoFileWidget clear due to pending restore" + ); + } + + if (this.imageFileWidget && !this._pendingFileRestore) { + console.log("[DEBUG] Clearing imageFileWidget"); + this.imageFileWidget.value = ""; + } else if (this.imageFileWidget && this._pendingFileRestore) { + console.log( + "[DEBUG] Skipping imageFileWidget clear due to pending restore" + ); + } + + // Also clear the original input widgets if not restoring + if (!this._pendingFileRestore) { + const originalUploadedImageWidget = this.widgets.find( + (w) => w.name === "uploaded_image_file" + ); + const originalUploadedVideoWidget = this.widgets.find( + (w) => w.name === "uploaded_video_file" + ); + + if (originalUploadedImageWidget) { + console.log("[DEBUG] Clearing uploaded_image_file widget"); + originalUploadedImageWidget.value = ""; + } + if (originalUploadedVideoWidget) { + console.log("[DEBUG] Clearing uploaded_video_file widget"); + originalUploadedVideoWidget.value = ""; + } + } else { + console.log( + "[DEBUG] Skipping original widget clear due to pending restore" + ); + } + + console.log("[DEBUG] clearAllMediaState completed"); + }; + + // Function to safely remove a widget + this.removeWidgetSafely = function (widget) { + if (widget) { + const index = this.widgets.indexOf(widget); + if (index !== -1) { + this.widgets.splice(index, 1); + } + } + }; + + // Function to update widgets based on media_source and media_type + this.updateMediaWidgets = function () { + const mediaSource = this.mediaSourceWidget?.value || "Upload Media"; + const mediaType = this.mediaTypeWidget?.value || "image"; + + console.log( + `[STATE] Updating widgets: mediaSource=${mediaSource}, mediaType=${mediaType}` + ); + console.log("[DEBUG] _pendingFileRestore exists:", !!this._pendingFileRestore); + + // Find the original input widgets that we want to control + const originalMediaPathWidget = this.widgets.find( + (w) => w.name === "media_path" + ); + const originalUploadedImageWidget = this.widgets.find( + (w) => w.name === "uploaded_image_file" + ); + const originalUploadedVideoWidget = this.widgets.find( + (w) => w.name === "uploaded_video_file" + ); + const originalSeedWidget = this.widgets.find((w) => w.name === "seed"); + + console.log("[DEBUG] Found widgets:"); + console.log(" originalMediaPathWidget:", !!originalMediaPathWidget); + console.log( + " originalUploadedImageWidget:", + !!originalUploadedImageWidget, + originalUploadedImageWidget?.value + ); + console.log( + " originalUploadedVideoWidget:", + !!originalUploadedVideoWidget, + originalUploadedVideoWidget?.value + ); + console.log(" originalSeedWidget:", !!originalSeedWidget); + + // Clear all previous media state when switching configurations + console.log("[STATE] About to call clearAllMediaState"); + this.clearAllMediaState(); + + // Remove all upload-related widgets first to ensure clean state + console.log("[STATE] Removing existing widgets..."); + this.removeWidgetSafely(this.imageUploadWidget); + this.removeWidgetSafely(this.imageInfoWidget); + this.removeWidgetSafely(this.videoUploadWidget); + this.removeWidgetSafely(this.videoInfoWidget); + // Don't remove the original media_path widget, just manage its visibility + // this.removeWidgetSafely(this.mediaPathWidget); + + // Reset widget references + console.log("[STATE] Resetting widget references"); + this.imageUploadWidget = null; + this.imageInfoWidget = null; + this.videoUploadWidget = null; + this.videoInfoWidget = null; + // this.mediaPathWidget = null; + + // Manage visibility of original input widgets + if (mediaSource === "Randomize Media from Path") { + console.log("[STATE] Showing media path widget"); + + // Show the original media_path widget + if (originalMediaPathWidget) { + originalMediaPathWidget.type = "text"; + originalMediaPathWidget.computeSize = + originalMediaPathWidget.constructor.prototype.computeSize; + this.mediaPathWidget = originalMediaPathWidget; // Reference the original + } + + // Show the seed widget for randomization + if (originalSeedWidget) { + originalSeedWidget.type = "number"; + originalSeedWidget.computeSize = + originalSeedWidget.constructor.prototype.computeSize; + console.log("[STATE] Showing seed widget for randomization"); + } + + // Note: Seed randomization is handled by ComfyUI's built-in controls + + // Hide upload file widgets + if (originalUploadedImageWidget) { + originalUploadedImageWidget.type = "hidden"; + originalUploadedImageWidget.computeSize = () => [0, -4]; + } + if (originalUploadedVideoWidget) { + originalUploadedVideoWidget.type = "hidden"; + originalUploadedVideoWidget.computeSize = () => [0, -4]; + } + } else { + // Upload Media mode - Show appropriate upload widgets based on media_type + console.log("[STATE] Upload Media mode - hiding media_path widget"); + + // Hide the original media_path widget + if (originalMediaPathWidget) { + originalMediaPathWidget.type = "hidden"; + originalMediaPathWidget.computeSize = () => [0, -4]; + } + + // Hide the seed widget when not randomizing + if (originalSeedWidget) { + originalSeedWidget.type = "hidden"; + originalSeedWidget.computeSize = () => [0, -4]; + console.log("[STATE] Hiding seed widget for upload mode"); + } + + if (mediaType === "image") { + console.log("[STATE] Creating image upload widgets"); + + // Hide the video upload widget, show image upload widget reference + if (originalUploadedVideoWidget) { + originalUploadedVideoWidget.type = "hidden"; + originalUploadedVideoWidget.computeSize = () => [0, -4]; + } + if (originalUploadedImageWidget) { + originalUploadedImageWidget.type = "hidden"; // Keep hidden, we'll use a custom widget + originalUploadedImageWidget.computeSize = () => [0, -4]; + } + + // Add image upload widgets + this.imageUploadWidget = this.addWidget( + "button", + "📁 Choose Image to Upload", + "upload_image", + () => { + this.onImageUploadButtonPressed(); + } + ); + this.imageUploadWidget.serialize = false; + + // Add a widget to display the selected image info + this.imageInfoWidget = this.addWidget( + "text", + "image_file", + "No image selected", + () => {}, + {} + ); + this.imageInfoWidget.serialize = false; + } else if (mediaType === "video") { + console.log("[STATE] Creating video upload widgets"); + + // Hide the image upload widget, show video upload widget reference + if (originalUploadedImageWidget) { + originalUploadedImageWidget.type = "hidden"; + originalUploadedImageWidget.computeSize = () => [0, -4]; + } + if (originalUploadedVideoWidget) { + originalUploadedVideoWidget.type = "hidden"; // Keep hidden, we'll use a custom widget + originalUploadedVideoWidget.computeSize = () => [0, -4]; + } + + // Add video upload widgets + this.videoUploadWidget = this.addWidget( + "button", + "📁 Choose Video to Upload", + "upload_video", + () => { + this.onVideoUploadButtonPressed(); + } + ); + this.videoUploadWidget.serialize = false; + + // Add a widget to display the selected video info + this.videoInfoWidget = this.addWidget( + "text", + "video_file", + "No video selected", + () => {}, + {} + ); + this.videoInfoWidget.serialize = false; + } + } + + console.log( + `[STATE] Widget update complete. Total widgets: ${ + this.widgets?.length || 0 + }` + ); + + // Force node to recalculate size + this.setSize(this.computeSize()); + }; + + // Initial setup + this.updateMediaWidgets(); + + // Hook into media_source widget changes + if (this.mediaSourceWidget) { + const originalSourceCallback = this.mediaSourceWidget.callback; + this.mediaSourceWidget.callback = (value) => { + if (originalSourceCallback) + originalSourceCallback.call(this.mediaSourceWidget, value); + this.updateMediaWidgets(); + }; + } + + // Hook into media_type widget changes + if (this.mediaTypeWidget) { + const originalTypeCallback = this.mediaTypeWidget.callback; + this.mediaTypeWidget.callback = (value) => { + if (originalTypeCallback) + originalTypeCallback.call(this.mediaTypeWidget, value); + this.updateMediaWidgets(); + }; + } + + return result; + }; + + // Add onSerialize method to save UI state + const onSerialize = nodeType.prototype.onSerialize; + nodeType.prototype.onSerialize = function (o) { + const result = onSerialize?.apply(this, arguments); + + console.log("[DEBUG] onSerialize called - collecting state data"); + console.log("[DEBUG] uploadedVideoFile:", this.uploadedVideoFile); + console.log("[DEBUG] uploadedVideoSubfolder:", this.uploadedVideoSubfolder); + console.log("[DEBUG] videoInfoWidget value:", this.videoInfoWidget?.value); + console.log("[DEBUG] uploadedImageFile:", this.uploadedImageFile); + console.log("[DEBUG] uploadedImageSubfolder:", this.uploadedImageSubfolder); + console.log("[DEBUG] imageInfoWidget value:", this.imageInfoWidget?.value); + + // Save current widget state for persistence + o.widgets_values = o.widgets_values || []; + o.ui_state = { + media_source: this.mediaSourceWidget?.value || "Upload Media", + media_type: this.mediaTypeWidget?.value || "image", + // Add uploaded file persistence + uploaded_file_info: { + image: { + file: this.uploadedImageFile, + subfolder: this.uploadedImageSubfolder, + display: this.imageInfoWidget?.value, + }, + video: { + file: this.uploadedVideoFile, + subfolder: this.uploadedVideoSubfolder, + display: this.videoInfoWidget?.value, + }, + }, + }; + + console.log("[SERIALIZE] Saving UI state:", JSON.stringify(o.ui_state, null, 2)); + + // Also check the actual widget values in widgets_values array + console.log("[DEBUG] widgets_values array:", o.widgets_values); + + // Check if any widgets have video file info + if (this.widgets) { + const videoWidget = this.widgets.find((w) => w.name === "uploaded_video_file"); + const imageWidget = this.widgets.find((w) => w.name === "uploaded_image_file"); + console.log("[DEBUG] uploaded_video_file widget value:", videoWidget?.value); + console.log("[DEBUG] uploaded_image_file widget value:", imageWidget?.value); + } + + return result; + }; + + // Add onConfigure method to restore UI state + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function (o) { + const result = onConfigure?.apply(this, arguments); + + console.log("[DEBUG] onConfigure called with data:", o); + console.log("[DEBUG] ui_state found:", !!o.ui_state); + console.log("[DEBUG] widgets_values found:", !!o.widgets_values); + + // Restore UI state after widgets are created + if (o.ui_state) { + console.log( + "[CONFIGURE] Restoring UI state:", + JSON.stringify(o.ui_state, null, 2) + ); + + // Set widget values if they exist + if (this.mediaSourceWidget && o.ui_state.media_source) { + console.log( + "[DEBUG] Setting mediaSourceWidget to:", + o.ui_state.media_source + ); + this.mediaSourceWidget.value = o.ui_state.media_source; + } + if (this.mediaTypeWidget && o.ui_state.media_type) { + console.log("[DEBUG] Setting mediaTypeWidget to:", o.ui_state.media_type); + this.mediaTypeWidget.value = o.ui_state.media_type; + } + + // Store upload file info for later restoration (after updateMediaWidgets clears state) + this._pendingFileRestore = o.ui_state.uploaded_file_info; + console.log( + "[DEBUG] Stored _pendingFileRestore:", + JSON.stringify(this._pendingFileRestore, null, 2) + ); + + // Update UI to match restored state + setTimeout(() => { + console.log("[DEBUG] First timeout - calling updateMediaWidgets"); + this.updateMediaWidgets(); + + // FIXED: Restore uploaded file information after updateMediaWidgets has run + // Need a second timeout to ensure widgets are fully created + setTimeout(() => { + console.log("[DEBUG] Second timeout - starting file restoration"); + console.log( + "[DEBUG] _pendingFileRestore still exists:", + !!this._pendingFileRestore + ); + + if (this._pendingFileRestore) { + const fileInfo = this._pendingFileRestore; + console.log( + "[DEBUG] Processing fileInfo:", + JSON.stringify(fileInfo, null, 2) + ); + + // Debug: List all current widgets + console.log( + "[DEBUG] Current widgets:", + this.widgets.map((w) => ({ + name: w.name, + type: w.type, + value: w.value, + })) + ); + + // Restore image upload state + if (fileInfo.image?.file) { + console.log("[DEBUG] Restoring image state:", fileInfo.image); + this.uploadedImageFile = fileInfo.image.file; + this.uploadedImageSubfolder = fileInfo.image.subfolder; + if (this.imageInfoWidget && fileInfo.image.display) { + this.imageInfoWidget.value = fileInfo.image.display; + console.log( + "[DEBUG] Updated imageInfoWidget:", + this.imageInfoWidget.value + ); + } + + // Update the hidden widget with file path + const originalUploadedImageWidget = this.widgets.find( + (w) => w.name === "uploaded_image_file" + ); + if (originalUploadedImageWidget) { + originalUploadedImageWidget.value = `${this.uploadedImageSubfolder}/${this.uploadedImageFile}`; + console.log( + `[CONFIGURE] Restored image file: ${originalUploadedImageWidget.value}` + ); + } else { + console.log( + "[DEBUG] WARNING: uploaded_image_file widget not found!" + ); + } + } + + // FIXED: Restore video upload state with proper widget handling + if (fileInfo.video?.file) { + console.log("[DEBUG] Restoring video state:", fileInfo.video); + this.uploadedVideoFile = fileInfo.video.file; + this.uploadedVideoSubfolder = fileInfo.video.subfolder; + + console.log( + "[DEBUG] Set uploadedVideoFile:", + this.uploadedVideoFile + ); + console.log( + "[DEBUG] Set uploadedVideoSubfolder:", + this.uploadedVideoSubfolder + ); + + // Ensure video info widget exists and update it + if (this.videoInfoWidget) { + if (fileInfo.video.display) { + this.videoInfoWidget.value = fileInfo.video.display; + } else { + // Fallback display if display info is missing + this.videoInfoWidget.value = `${this.uploadedVideoFile} (restored)`; + } + console.log( + `[CONFIGURE] Restored video info widget: ${this.videoInfoWidget.value}` + ); + } else { + console.log("[DEBUG] WARNING: videoInfoWidget not found!"); + } + + // Update the hidden widget with file path + const originalUploadedVideoWidget = this.widgets.find( + (w) => w.name === "uploaded_video_file" + ); + if (originalUploadedVideoWidget) { + const filePath = `${this.uploadedVideoSubfolder}/${this.uploadedVideoFile}`; + originalUploadedVideoWidget.value = filePath; + console.log( + `[CONFIGURE] Restored video file widget: ${originalUploadedVideoWidget.value}` + ); + } else { + console.log( + "[DEBUG] WARNING: uploaded_video_file widget not found!" + ); + } + + // Also ensure the videoFileWidget is updated if it exists + if (this.videoFileWidget) { + this.videoFileWidget.value = `${this.uploadedVideoSubfolder}/${this.uploadedVideoFile}`; + console.log( + `[CONFIGURE] Updated videoFileWidget: ${this.videoFileWidget.value}` + ); + } else { + console.log( + "[DEBUG] videoFileWidget not found (this might be OK)" + ); + } + } else { + console.log("[DEBUG] No video file info to restore"); + } + + // Clean up temporary storage + delete this._pendingFileRestore; + console.log("[DEBUG] Cleaned up _pendingFileRestore"); + } else { + console.log( + "[DEBUG] No _pendingFileRestore found in second timeout" + ); + } + + console.log("[CONFIGURE] File state restoration complete"); + }, 50); // Small delay to ensure widget creation is complete + + console.log("[CONFIGURE] UI state restored and widgets updated"); + }, 0); + } else { + console.log("[CONFIGURE] No UI state found, using defaults"); + // Ensure initial state is applied even without saved state + setTimeout(() => { + this.updateMediaWidgets(); + }, 0); + } + + return result; + }; + + // Add onExecuted method to update the final_string widget + const onExecutedMedia = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + const result = onExecutedMedia?.apply(this, arguments); + return result; + }; + + // Method to clear current media type state only + nodeType.prototype.clearCurrentMediaState = function () { + const mediaType = this.mediaTypeWidget?.value || "image"; + + if (mediaType === "video") { + // Clear video state + this.clearVideoPreview(); + this.uploadedVideoFile = null; + this.uploadedVideoSubfolder = null; + + if (this.videoInfoWidget) { + this.videoInfoWidget.value = "No video selected"; + } + if (this.videoFileWidget) { + this.videoFileWidget.value = ""; + } + } else if (mediaType === "image") { + // Clear image state + this.uploadedImageFile = null; + this.uploadedImageSubfolder = null; + + if (this.imageInfoWidget) { + this.imageInfoWidget.value = "No image selected"; + } + if (this.imageFileWidget) { + this.imageFileWidget.value = ""; + } + } + }; + + // Add image upload handler + nodeType.prototype.onImageUploadButtonPressed = function () { + console.log("Image upload button pressed!"); + + // Clear current image state before starting new upload + this.clearCurrentMediaState(); + + // Create file input element + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "image/*"; + fileInput.style.display = "none"; + + fileInput.onchange = async (event) => { + const file = event.target.files[0]; + if (!file) { + return; + } + + // Validate file type + if (!file.type.startsWith("image/")) { + app.ui.dialog.show("Please select a valid image file."); + return; + } + + // Show loading state + this.imageInfoWidget.value = "Uploading image..."; + + try { + // Upload the image file + const formData = new FormData(); + formData.append("image", file); + formData.append("subfolder", "gemini_images"); + formData.append("type", "input"); + + const uploadResponse = await fetch("/upload/image", { + method: "POST", + body: formData, + }); + + if (!uploadResponse.ok) { + throw new Error(`Upload failed: ${uploadResponse.statusText}`); + } + + const uploadResult = await uploadResponse.json(); + + // Update the image info widget + this.imageInfoWidget.value = `${file.name} (${( + file.size / + 1024 / + 1024 + ).toFixed(2)} MB)`; + + // Store image info for processing + this.uploadedImageFile = uploadResult.name; + this.uploadedImageSubfolder = uploadResult.subfolder || "gemini_images"; + + // Use the original uploaded_image_file widget to store the file path + const originalUploadedImageWidget = this.widgets.find( + (w) => w.name === "uploaded_image_file" + ); + if (originalUploadedImageWidget) { + originalUploadedImageWidget.value = `${this.uploadedImageSubfolder}/${this.uploadedImageFile}`; + console.log( + `[UPLOAD] Updated original uploaded_image_file widget: ${originalUploadedImageWidget.value}` + ); + } else { + // Fallback: create a hidden widget if the original doesn't exist + if (!this.imageFileWidget) { + this.imageFileWidget = this.addWidget( + "text", + "uploaded_image_file", + "", + () => {}, + {} + ); + this.imageFileWidget.serialize = true; + this.imageFileWidget.type = "hidden"; + } + this.imageFileWidget.value = `${this.uploadedImageSubfolder}/${this.uploadedImageFile}`; + } + + // Show success notification + app.extensionManager?.toast?.add({ + severity: "success", + summary: "Image Upload", + detail: `Successfully uploaded ${file.name}`, + life: 3000, + }); + + console.log("Image uploaded:", uploadResult); + } catch (error) { + console.error("Upload error:", error); + + // Clear everything on error + this.imageInfoWidget.value = "Upload failed"; + this.uploadedImageFile = null; + this.uploadedImageSubfolder = null; + + if (this.imageFileWidget) { + this.imageFileWidget.value = ""; + } + + app.ui.dialog.show(`Upload failed: ${error.message}`); + } + + // Clean up + document.body.removeChild(fileInput); + }; + + // Trigger file selection + document.body.appendChild(fileInput); + fileInput.click(); + }; + + // Add video upload handler (reuse from existing video node) + nodeType.prototype.onVideoUploadButtonPressed = function () { + console.log("Video upload button pressed!"); + + // Clear current video state before starting new upload + this.clearCurrentMediaState(); + + // Create file input element + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "video/*"; + fileInput.style.display = "none"; + + fileInput.onchange = async (event) => { + const file = event.target.files[0]; + if (!file) { + // User cancelled, keep cleared state + return; + } + + // Validate file type + if (!file.type.startsWith("video/")) { + app.ui.dialog.show("Please select a valid video file."); + return; + } + + // Show loading state + this.videoInfoWidget.value = "Uploading video..."; + + try { + // Upload the video file + const formData = new FormData(); + formData.append("image", file); + formData.append("subfolder", "gemini_videos"); + formData.append("type", "input"); + + const uploadResponse = await fetch("/upload/image", { + method: "POST", + body: formData, + }); + + if (!uploadResponse.ok) { + throw new Error(`Upload failed: ${uploadResponse.statusText}`); + } + + const uploadResult = await uploadResponse.json(); + + console.log("[DEBUG] Video upload successful, result:", uploadResult); + + // Update the video info widget + this.videoInfoWidget.value = `${file.name} (${( + file.size / + 1024 / + 1024 + ).toFixed(2)} MB)`; + + // Store video info for processing + this.uploadedVideoFile = uploadResult.name; + this.uploadedVideoSubfolder = uploadResult.subfolder || "gemini_videos"; + + console.log("[DEBUG] Set uploadedVideoFile to:", this.uploadedVideoFile); + console.log( + "[DEBUG] Set uploadedVideoSubfolder to:", + this.uploadedVideoSubfolder + ); + console.log("[DEBUG] Set videoInfoWidget to:", this.videoInfoWidget.value); + + // Use the original uploaded_video_file widget to store the file path + const originalUploadedVideoWidget = this.widgets.find( + (w) => w.name === "uploaded_video_file" + ); + if (originalUploadedVideoWidget) { + originalUploadedVideoWidget.value = `${this.uploadedVideoSubfolder}/${this.uploadedVideoFile}`; + console.log( + `[UPLOAD] Updated original uploaded_video_file widget: ${originalUploadedVideoWidget.value}` + ); + } else { + console.log( + "[DEBUG] WARNING: uploaded_video_file widget not found during upload!" + ); + // Fallback: create a hidden widget if the original doesn't exist + if (!this.videoFileWidget) { + this.videoFileWidget = this.addWidget( + "text", + "uploaded_video_file", + "", + () => {}, + {} + ); + this.videoFileWidget.serialize = true; + this.videoFileWidget.type = "hidden"; + console.log("[DEBUG] Created fallback videoFileWidget"); + } + this.videoFileWidget.value = `${this.uploadedVideoSubfolder}/${this.uploadedVideoFile}`; + console.log( + "[DEBUG] Updated fallback videoFileWidget:", + this.videoFileWidget.value + ); + } + + // Debug: Show all widget states after upload + console.log("[DEBUG] All widgets after upload:"); + this.widgets.forEach((w) => { + console.log(` ${w.name}: ${w.value} (type: ${w.type})`); + }); + + // Show success notification + app.extensionManager?.toast?.add({ + severity: "success", + summary: "Video Upload", + detail: `Successfully uploaded ${file.name}`, + life: 3000, + }); + + console.log("Video uploaded:", uploadResult); + } catch (error) { + console.error("Upload error:", error); + + // Clear everything on error + this.clearVideoPreview(); + this.videoInfoWidget.value = "Upload failed"; + this.uploadedVideoFile = null; + this.uploadedVideoSubfolder = null; + + if (this.videoFileWidget) { + this.videoFileWidget.value = ""; + } + + app.ui.dialog.show(`Upload failed: ${error.message}`); + } + + // Clean up + document.body.removeChild(fileInput); + }; + + // Trigger file selection + document.body.appendChild(fileInput); + fileInput.click(); + }; + + // Add video preview clearing method (minimal version for media node) + nodeType.prototype.clearVideoPreview = function () { + // For the media node, we don't have complex video preview + // This is just a placeholder method + console.log("Video preview cleared for media node"); + }; + } + }, + + // Hook to handle workflow loading + loadedGraphNode(node, app) { + if (node.comfyClass === "GeminiUtilMediaDescribe") { + console.log("[LOADED] loadedGraphNode called for GeminiUtilMediaDescribe"); + + // Check if this node has saved UI state with uploaded file data + const hasSavedVideoData = node.ui_state?.uploaded_file_info?.video?.file; + const hasSavedImageData = node.ui_state?.uploaded_file_info?.image?.file; + + console.log( + "[LOADED] hasSavedVideoData:", + !!hasSavedVideoData, + "hasSavedImageData:", + !!hasSavedImageData + ); + console.log("[LOADED] Saved video file:", hasSavedVideoData); + console.log("[LOADED] Saved image file:", hasSavedImageData); + + // Only call updateMediaWidgets if we don't have any saved uploaded file data + // If we have saved data, onConfigure will handle the restoration + if (!hasSavedVideoData && !hasSavedImageData && node.updateMediaWidgets) { + console.log( + "[LOADED] No saved uploaded file data found, applying default UI state" + ); + setTimeout(() => { + node.updateMediaWidgets(); + console.log("[LOADED] Applied default UI state for loaded workflow node"); + }, 100); // Small delay to ensure all widgets are properly initialized + } else { + console.log( + "[LOADED] Saved uploaded file data found, skipping updateMediaWidgets to preserve onConfigure restoration" + ); + } + } + }, +}); diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..bb35280 --- /dev/null +++ b/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "comfyui-swissarmyknife-web", + "version": "1.0.0", + "description": "Web extension for ComfyUI-SwissArmyKnife - JavaScript widgets for ComfyUI", + "scripts": { + "dev": "echo 'JavaScript widgets - no build step required. Files are in js/ and css/ directories.'", + "build": "echo 'JavaScript widgets - no build step required. Files are loaded directly by ComfyUI.'", + "lint": "eslint js/**/*.js", + "lint:fix": "eslint js/**/*.js --fix", + "format": "prettier --write js/**/*.js css/**/*.css", + "test": "cd tests && npm test" + }, + "devDependencies": { + "eslint": "^9.0.0", + "prettier": "^3.0.0" + }, + "keywords": [ + "comfyui", + "widgets", + "javascript", + "extension" + ], + "author": "Sam Kumar ", + "license": "GPL-3.0" +} diff --git a/web/tests/README.md b/web/tests/README.md new file mode 100644 index 0000000..3d06d39 --- /dev/null +++ b/web/tests/README.md @@ -0,0 +1,146 @@ +# ComfyUI-SwissArmyKnife - Playwright Tests + +This directory contains Playwright-based browser automation tests for the ComfyUI-SwissArmyKnife ComfyUI extension. + +## Test Suite Overview + +### Tests Included + +1. **`test_state_management.js`** - Comprehensive state management testing + + - Tests widget visibility when switching between image/video modes + - Verifies only appropriate upload buttons are shown + - Tests transitions between Upload Media and Randomize Media modes + - 6 comprehensive test scenarios + +2. **`test_media_path_fix.js`** - Media path preservation testing + + - Tests that media_path values are preserved during widget recreation + - Verifies the fix for the "Media path is required" error + - Tests widget state transitions + +3. **`quick_test.js`** - Quick validation test + - Simplified test for rapid verification + - Tests basic image → video → image transitions + - Faster execution for quick checks + +## Setup + +### Install Dependencies + +```bash +cd web/tests +npm install +``` + +### Install Browser + +```bash +npm run install-browser +``` + +## Running Tests + +### Run All Tests + +```bash +npm test +# or +npm run test:all +``` + +### Run Individual Tests + +```bash +# State management tests +npm run test:state + +# Media path preservation tests +npm run test:media-path + +# Quick validation test +npm run test:quick +``` + +### Headless Mode + +```bash +npm run test:headless +``` + +## Test Requirements + +- **ComfyUI Instance**: Tests connect to `https://comfyui.devlabhq.com/` +- **Workflow File**: Requires `video-describe.json` workflow to be available +- **Node Type**: Tests the `GeminiUtilMediaDescribe` node specifically +- **Browser**: Chromium (installed automatically via Playwright) + +## Test Structure + +Each test follows this pattern: + +1. **Setup**: Launch browser and navigate to ComfyUI +2. **Workflow Loading**: Open the video-describe workflow +3. **Node Interaction**: Find and interact with the Gemini node +4. **State Testing**: Perform various state transitions +5. **Validation**: Check widget visibility and values +6. **Cleanup**: Close browser + +## Understanding Test Output + +### Success Indicators + +- ✅ **PASSED** - Individual test case passed +- 🎉 **ALL TESTS PASSED** - Complete test suite success + +### Debug Information + +- 📝 **ComfyUI Console** - State change logs from the extension +- 📊 **Test Results Summary** - Overall pass/fail statistics +- 🔍 **Widget State Info** - Current widget configuration details + +## Troubleshooting + +### Common Issues + +1. **"Target page has been closed"** + + - Ensure running in headless mode on servers without X11 + - Check that ComfyUI is accessible at the test URL + +2. **"No tests ran"** + + - Verify the workflow file exists in ComfyUI + - Check that the GeminiUtilMediaDescribe node is available + +3. **Widget state tests failing** + - Clear browser cache and restart ComfyUI + - Verify the latest JavaScript widgets are loaded + +### Test Timeouts + +- Navigation: 60 seconds +- Widget interactions: 1-2 seconds +- State transitions: 1-1.5 seconds +- Complete test suite: ~2-3 minutes + +## Development + +### Adding New Tests + +1. Create new test file in this directory +2. Follow the existing class structure pattern +3. Add npm script to `package.json` +4. Update this README + +### Test Data + +Tests use these standard values: + +- **Test paths**: `/test/media/path`, `/reference-vids/jg-reels` +- **Media types**: `image`, `video` +- **Media sources**: `Upload Media`, `Randomize Media from Path` + +### Console Logging + +Tests listen for console messages with `[STATE]` prefix to track widget operations during state transitions. diff --git a/web/tests/package-lock.json b/web/tests/package-lock.json new file mode 100644 index 0000000..401d627 --- /dev/null +++ b/web/tests/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "comfyui-swissarmyknife-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "comfyui-swissarmyknife-tests", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "playwright": "^1.40.0" + }, + "devDependencies": {} + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/web/tests/package.json b/web/tests/package.json new file mode 100644 index 0000000..8ab4c5b --- /dev/null +++ b/web/tests/package.json @@ -0,0 +1,34 @@ +{ + "name": "comfyui-swissarmyknife-tests", + "version": "1.0.0", + "description": "Playwright test suite for ComfyUI-SwissArmyKnife ComfyUI extension", + "main": "test_state_management.js", + "scripts": { + "test": "npm run test:all", + "test:all": "npm run test:state && npm run test:media-path && npm run test:quick", + "test:state": "node test_state_management.js", + "test:media-path": "node test_media_path_fix.js", + "test:quick": "node quick_test.js", + "test:headless": "HEADLESS=true npm run test:all", + "install-browser": "npx playwright install chromium" + }, + "dependencies": { + "playwright": "^1.40.0" + }, + "devDependencies": {}, + "keywords": [ + "comfyui", + "playwright", + "testing", + "gemini", + "widgets", + "state-management" + ], + "author": "ComfyUI-SwissArmyKnife", + "license": "GPL-3.0", + "homepage": "https://github.com/sammykumar/ComfyUI-SwissArmyKnife#readme", + "repository": { + "type": "git", + "url": "https://github.com/sammykumar/ComfyUI-SwissArmyKnife.git" + } +}