diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..a06cb1d114 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,40 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: ["*"] + + - package-ecosystem: "pip" + directories: + - "/" + - "/studio/backend/plugins/data-designer-unstructured-seed" + - "/studio/backend/requirements" + - "/unsloth/kernels/moe" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + groups: + pip: + patterns: ["*"] + + - package-ecosystem: "bun" + directory: "/studio/frontend" + schedule: + interval: "weekly" + groups: + bun-frontend: + patterns: ["*"] + + - package-ecosystem: "npm" + directory: "/studio/backend/core/data_recipe/oxc-validator" + schedule: + interval: "weekly" + groups: + npm-oxc-validator: + patterns: ["*"] +... diff --git a/build.sh b/build.sh index 3118e8810a..cf8aa02910 100644 --- a/build.sh +++ b/build.sh @@ -29,7 +29,22 @@ _restore_gitignores() { } trap _restore_gitignores EXIT -npm install +# Use bun for install if available (faster), fall back to npm. +_install_ok=false +if command -v bun &>/dev/null; then + if bun install; then + _install_ok=true + else + echo "⚠ bun install failed, falling back to npm" + rm -rf node_modules + fi +fi +if [ "$_install_ok" != "true" ]; then + if ! npm install; then + echo "❌ ERROR: package install failed" >&2 + exit 1 + fi +fi npm run build # outputs to studio/frontend/dist/ _restore_gitignores diff --git a/install.ps1 b/install.ps1 index d438e5ed2d..a4ed2658c9 100644 --- a/install.ps1 +++ b/install.ps1 @@ -5,8 +5,9 @@ function Install-UnslothStudio { $ErrorActionPreference = "Stop" - $VenvName = "unsloth_studio" $PythonVersion = "3.13" + $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio" + $VenvDir = Join-Path $StudioHome "unsloth_studio" Write-Host "" Write-Host "=========================================" @@ -31,6 +32,275 @@ function Install-UnslothStudio { $env:Path = $unique -join ";" } + function New-StudioShortcuts { + param( + [Parameter(Mandatory = $true)][string]$UnslothExePath + ) + + if (-not (Test-Path $UnslothExePath)) { + Write-Host "[WARN] Cannot create shortcuts: unsloth.exe not found at $UnslothExePath" -ForegroundColor Yellow + return + } + try { + # Persist an absolute path in launcher scripts so shortcut working + # directory changes do not break process startup. + $UnslothExePath = (Resolve-Path $UnslothExePath).Path + # Escape for single-quoted embedding in generated launcher script. + # This prevents runtime variable expansion for paths containing '$'. + $SingleQuotedExePath = $UnslothExePath -replace "'", "''" + + $localAppDataDir = $env:LOCALAPPDATA + if (-not $localAppDataDir -or [string]::IsNullOrWhiteSpace($localAppDataDir)) { + Write-Host "[WARN] LOCALAPPDATA path unavailable; skipped shortcut creation" -ForegroundColor Yellow + return + } + $appDir = Join-Path $localAppDataDir "Unsloth Studio" + $launcherPs1 = Join-Path $appDir "launch-studio.ps1" + $launcherVbs = Join-Path $appDir "launch-studio.vbs" + $desktopDir = [Environment]::GetFolderPath("Desktop") + $desktopLink = if ($desktopDir -and $desktopDir.Trim()) { + Join-Path $desktopDir "Unsloth Studio.lnk" + } else { + $null + } + $startMenuDir = if ($env:APPDATA -and $env:APPDATA.Trim()) { + Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs" + } else { + $null + } + $startMenuLink = if ($startMenuDir -and $startMenuDir.Trim()) { + Join-Path $startMenuDir "Unsloth Studio.lnk" + } else { + $null + } + if (-not $desktopLink) { + Write-Host "[WARN] Desktop path unavailable; skipped desktop shortcut creation" -ForegroundColor Yellow + } + if (-not $startMenuLink) { + Write-Host "[WARN] APPDATA/Start Menu path unavailable; skipped Start menu shortcut creation" -ForegroundColor Yellow + } + $iconPath = Join-Path $appDir "unsloth.ico" + $bundledIcon = $null + if ($PSScriptRoot -and $PSScriptRoot.Trim()) { + $bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico" + } + $iconUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth.ico" + + if (-not (Test-Path $appDir)) { + New-Item -ItemType Directory -Path $appDir -Force | Out-Null + } + + $launcherContent = @" +`$ErrorActionPreference = 'Stop' +`$basePort = 8888 +`$maxPortOffset = 20 +`$timeoutSec = 60 +`$pollIntervalMs = 1000 + +function Test-StudioHealth { + param([Parameter(Mandatory = `$true)][int]`$Port) + try { + `$url = "http://127.0.0.1:`$Port/api/health" + `$resp = Invoke-RestMethod -Uri `$url -TimeoutSec 1 -Method Get + return (`$resp -and `$resp.status -eq 'healthy' -and `$resp.service -eq 'Unsloth UI Backend') + } catch { + return `$false + } +} + +function Get-CandidatePorts { + # Fast path: only probe base port + currently listening ports in range. + `$ports = @(`$basePort) + try { + `$maxPort = `$basePort + `$maxPortOffset + `$listening = Get-NetTCPConnection -State Listen -ErrorAction Stop | + Where-Object { `$_.LocalPort -ge `$basePort -and `$_.LocalPort -le `$maxPort } | + Select-Object -ExpandProperty LocalPort + `$ports = (@(`$basePort) + `$listening) | Sort-Object -Unique + } catch { + Write-Host "[DEBUG] Get-NetTCPConnection failed: `$(`$_.Exception.Message). Falling back to full port scan." -ForegroundColor DarkGray + # Fallback when Get-NetTCPConnection is unavailable/restricted. + for (`$offset = 1; `$offset -le `$maxPortOffset; `$offset++) { + `$ports += (`$basePort + `$offset) + } + } + return `$ports +} + +function Find-HealthyStudioPort { + foreach (`$candidate in (Get-CandidatePorts)) { + if (Test-StudioHealth -Port `$candidate) { + return `$candidate + } + } + return `$null +} + +# If Studio is already healthy on any expected port, just open it and exit. +`$existingPort = Find-HealthyStudioPort +if (`$existingPort) { + Start-Process "http://localhost:`$existingPort" + exit 0 +} + +`$launchMutex = [System.Threading.Mutex]::new(`$false, 'Local\UnslothStudioLauncher') +`$haveMutex = `$false +try { + try { + `$haveMutex = `$launchMutex.WaitOne(0) + } catch [System.Threading.AbandonedMutexException] { + `$haveMutex = `$true + } + if (-not `$haveMutex) { + # Another launcher is already running; wait for it to bring Studio up + `$deadline = (Get-Date).AddSeconds(`$timeoutSec) + while ((Get-Date) -lt `$deadline) { + `$port = Find-HealthyStudioPort + if (`$port) { Start-Process "http://localhost:`$port"; exit 0 } + Start-Sleep -Milliseconds `$pollIntervalMs + } + exit 0 + } + + `$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + `$studioExe = '$SingleQuotedExePath' + `$studioCommand = '& "' + `$studioExe + '" studio -H 0.0.0.0 -p ' + `$basePort + `$launchArgs = @( + '-NoExit', + '-NoProfile', + '-ExecutionPolicy', + 'Bypass', + '-Command', + `$studioCommand + ) + + try { + `$proc = Start-Process -FilePath `$powershellExe -ArgumentList `$launchArgs -WorkingDirectory `$env:USERPROFILE -PassThru + } catch { + `$msg = "Could not launch Unsloth Studio terminal.`n`nError: `$(`$_.Exception.Message)" + try { + Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop + [System.Windows.Forms.MessageBox]::Show(`$msg, 'Unsloth Studio') | Out-Null + } catch {} + exit 1 + } + + `$browserOpened = `$false + `$deadline = (Get-Date).AddSeconds(`$timeoutSec) + while ((Get-Date) -lt `$deadline) { + `$healthyPort = Find-HealthyStudioPort + if (`$healthyPort) { + Start-Process "http://localhost:`$healthyPort" + `$browserOpened = `$true + break + } + if (`$proc.HasExited) { break } + Start-Sleep -Milliseconds `$pollIntervalMs + } + if (-not `$browserOpened) { + if (`$proc.HasExited) { + `$msg = "Unsloth Studio exited before becoming healthy. Check terminal output for errors." + } else { + `$msg = "Unsloth Studio is still starting but did not become healthy within `$timeoutSec seconds. Check the terminal window for the selected port and open it manually." + } + try { + Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop + [System.Windows.Forms.MessageBox]::Show(`$msg, 'Unsloth Studio') | Out-Null + } catch {} + } +} finally { + if (`$haveMutex) { `$launchMutex.ReleaseMutex() | Out-Null } + `$launchMutex.Dispose() +} +exit 0 +"@ + + # Write UTF-8 with BOM for reliable decoding by Windows PowerShell 5.1, + # even when install.ps1 is executed from PowerShell 7. + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($launcherPs1, $launcherContent, $utf8Bom) + $vbsContent = @" +Set shell = CreateObject("WScript.Shell") +cmd = "powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""$launcherPs1""" +shell.Run cmd, 0, False +"@ + # WSH handles UTF-16LE reliably for .vbs files with non-ASCII paths. + Set-Content -Path $launcherVbs -Value $vbsContent -Encoding Unicode -Force + + # Prefer bundled icon from local clone/dev installs. + # If not available, best-effort download from raw GitHub. + # We only attach the icon if the resulting file has a valid ICO header. + $hasValidIcon = $false + if ($bundledIcon -and (Test-Path $bundledIcon)) { + try { + Copy-Item -Path $bundledIcon -Destination $iconPath -Force + } catch { + Write-Host "[DEBUG] Error copying bundled icon: $($_.Exception.Message)" -ForegroundColor DarkGray + } + } elseif (-not (Test-Path $iconPath)) { + try { + Invoke-WebRequest -Uri $iconUrl -OutFile $iconPath -UseBasicParsing + } catch { + Write-Host "[DEBUG] Error downloading icon: $($_.Exception.Message)" -ForegroundColor DarkGray + } + } + + if (Test-Path $iconPath) { + try { + $bytes = [System.IO.File]::ReadAllBytes($iconPath) + if ( + $bytes.Length -ge 4 -and + $bytes[0] -eq 0 -and + $bytes[1] -eq 0 -and + $bytes[2] -eq 1 -and + $bytes[3] -eq 0 + ) { + $hasValidIcon = $true + } else { + Remove-Item $iconPath -Force -ErrorAction SilentlyContinue + } + } catch { + Write-Host "[DEBUG] Error validating or removing icon: $($_.Exception.Message)" -ForegroundColor DarkGray + Remove-Item $iconPath -Force -ErrorAction SilentlyContinue + } + } + + $wscriptExe = Join-Path $env:SystemRoot "System32\wscript.exe" + $shortcutArgs = "//B //Nologo `"$launcherVbs`"" + + try { + $wshell = New-Object -ComObject WScript.Shell + $createdShortcutCount = 0 + foreach ($linkPath in @($desktopLink, $startMenuLink)) { + if (-not $linkPath -or [string]::IsNullOrWhiteSpace($linkPath)) { continue } + try { + $shortcut = $wshell.CreateShortcut($linkPath) + $shortcut.TargetPath = $wscriptExe + $shortcut.Arguments = $shortcutArgs + $shortcut.WorkingDirectory = $appDir + $shortcut.Description = "Launch Unsloth Studio" + if ($hasValidIcon) { + $shortcut.IconLocation = "$iconPath,0" + } + $shortcut.Save() + $createdShortcutCount++ + } catch { + Write-Host "[WARN] Could not create shortcut at ${linkPath}: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + if ($createdShortcutCount -gt 0) { + Write-Host "[OK] Created Unsloth Studio shortcut(s): $createdShortcutCount" -ForegroundColor Green + } else { + Write-Host "[WARN] No Unsloth Studio shortcuts were created" -ForegroundColor Yellow + } + } catch { + Write-Host "[WARN] Shortcut creation unavailable: $($_.Exception.Message)" -ForegroundColor Yellow + } + } catch { + Write-Host "[WARN] Shortcut setup failed; skipping shortcuts: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + # ── Check winget ── if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Host "Error: winget is not available." -ForegroundColor Red @@ -180,20 +450,59 @@ function Install-UnslothStudio { return } - # ── Create venv (skip if it already exists and has a valid interpreter) ── + # ── Create venv (migrate old layout if possible, otherwise fresh) ── # Pass the resolved executable path to uv so it does not re-resolve # a version string back to a conda interpreter. - $VenvPython = Join-Path $VenvName "Scripts\python.exe" + if (-not (Test-Path $StudioHome)) { + New-Item -ItemType Directory -Path $StudioHome -Force | Out-Null + } + + $VenvPython = Join-Path $VenvDir "Scripts\python.exe" + $_Migrated = $false + + if (Test-Path $VenvPython) { + # New layout already exists -- nuke for fresh install + Write-Host "==> Removing existing environment for fresh install..." + Remove-Item -Recurse -Force $VenvDir + } elseif (Test-Path (Join-Path $StudioHome ".venv\Scripts\python.exe")) { + # Old layout (~/.unsloth/studio/.venv) exists -- validate before migrating + $OldVenv = Join-Path $StudioHome ".venv" + $OldPy = Join-Path $OldVenv "Scripts\python.exe" + Write-Host "==> Found legacy Studio environment, validating..." + $prevEAP2 = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + & $OldPy -c "import torch; A = torch.ones((2,2)); B = A + A" 2>$null | Out-Null + $torchOk = ($LASTEXITCODE -eq 0) + } catch { $torchOk = $false } + $ErrorActionPreference = $prevEAP2 + if ($torchOk) { + Write-Host " Legacy environment is healthy -- migrating..." + Move-Item -Path $OldVenv -Destination $VenvDir -Force + Write-Host " Moved .venv -> unsloth_studio" + $_Migrated = $true + } else { + Write-Host " Legacy environment failed validation -- creating fresh environment" + Remove-Item -Recurse -Force $OldVenv -ErrorAction SilentlyContinue + } + } elseif (Test-Path (Join-Path $env:USERPROFILE "unsloth_studio\Scripts\python.exe")) { + # CWD-relative venv from old install.ps1 -- migrate to absolute path + $CwdVenv = Join-Path $env:USERPROFILE "unsloth_studio" + Write-Host "==> Found CWD-relative Studio environment, migrating to $VenvDir..." + Move-Item -Path $CwdVenv -Destination $VenvDir -Force + Write-Host " Moved ~/unsloth_studio -> ~/.unsloth/studio/unsloth_studio" + $_Migrated = $true + } + if (-not (Test-Path $VenvPython)) { - if (Test-Path $VenvName) { Remove-Item -Recurse -Force $VenvName } - Write-Host "==> Creating Python $($DetectedPython.Version) virtual environment (${VenvName})..." - uv venv $VenvName --python "$($DetectedPython.Path)" + Write-Host "==> Creating Python $($DetectedPython.Version) virtual environment ($VenvDir)..." + uv venv $VenvDir --python "$($DetectedPython.Path)" if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] Failed to create virtual environment (exit code $LASTEXITCODE)" -ForegroundColor Red return } } else { - Write-Host "==> Virtual environment ${VenvName} already exists, skipping creation." + Write-Host "==> Using migrated environment at $VenvDir" } # ── Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) ── @@ -267,15 +576,26 @@ function Install-UnslothStudio { # CUDA wheels. Missing dependencies (transformers, trl, peft, etc.) # are still pulled in because they are new, not upgrades. # - Write-Host "==> Installing PyTorch ($TorchIndexUrl)..." - uv pip install --python $VenvPython torch torchvision torchaudio --index-url $TorchIndexUrl - if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] Failed to install PyTorch (exit code $LASTEXITCODE)" -ForegroundColor Red - return - } + if ($_Migrated) { + # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state + # in the new venv location, while preserving existing torch/CUDA + Write-Host "==> Upgrading unsloth in migrated environment..." + uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.3.11" unsloth-zoo + } elseif ($TorchIndexUrl) { + Write-Host "==> Installing PyTorch ($TorchIndexUrl)..." + uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to install PyTorch (exit code $LASTEXITCODE)" -ForegroundColor Red + return + } - Write-Host "==> Installing unsloth (this may take a few minutes)..." - uv pip install --python $VenvPython --upgrade-package unsloth unsloth + Write-Host "==> Installing unsloth (this may take a few minutes)..." + uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.3.11" + } else { + # Fallback: GPU detection failed to produce a URL -- let uv resolve torch + Write-Host "==> Installing unsloth (this may take a few minutes)..." + uv pip install --python $VenvPython "unsloth>=2026.3.11" --torch-backend=auto + } if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $LASTEXITCODE)" -ForegroundColor Red return @@ -285,7 +605,7 @@ function Install-UnslothStudio { # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, # CUDA Toolkit, Node.js, and other dependencies automatically via winget. Write-Host "==> Running unsloth studio setup..." - $UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe" + $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe" if (-not (Test-Path $UnslothExe)) { Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red Write-Host " Expected: $UnslothExe" -ForegroundColor Yellow @@ -293,12 +613,16 @@ function Install-UnslothStudio { Write-Host " Try re-running the installer or see: https://github.com/unslothai/unsloth?tab=readme-ov-file#-quickstart" -ForegroundColor Yellow return } + # Tell setup.ps1 to skip base package installation (install.ps1 already did it) + $env:SKIP_STUDIO_BASE = "1" & $UnslothExe studio setup if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] unsloth studio setup failed (exit code $LASTEXITCODE)" -ForegroundColor Red return } + New-StudioShortcuts -UnslothExePath $UnslothExe + Write-Host "" Write-Host "=========================================" Write-Host " Unsloth Studio installed!" @@ -311,12 +635,11 @@ function Install-UnslothStudio { if ($IsInteractive) { Write-Host "==> Launching Unsloth Studio..." Write-Host "" - $UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe" & $UnslothExe studio -H 0.0.0.0 -p 8888 } else { Write-Host " To launch, run:" Write-Host "" - Write-Host " .\${VenvName}\Scripts\activate" + Write-Host " & `"$VenvDir\Scripts\Activate.ps1`"" Write-Host " unsloth studio -H 0.0.0.0 -p 8888" Write-Host "" } diff --git a/install.sh b/install.sh index 18db3469f4..6f60c23d27 100755 --- a/install.sh +++ b/install.sh @@ -1,11 +1,35 @@ #!/bin/sh # Unsloth Studio Installer -# Usage (curl): curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh -# Usage (wget): wget -qO- https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh +# Usage (curl): curl -fsSL https://unsloth.ai/install.sh | sh +# Usage (wget): wget -qO- https://unsloth.ai/install.sh | sh +# Usage (local): ./install.sh --local (install from local repo instead of PyPI) +# Usage (test): ./install.sh --package roland-sloth (install a different package name) set -e -VENV_NAME="unsloth_studio" +# ── Parse flags ── +STUDIO_LOCAL_INSTALL=false +PACKAGE_NAME="unsloth" +_next_is_package=false +for arg in "$@"; do + if [ "$_next_is_package" = true ]; then + PACKAGE_NAME="$arg" + _next_is_package=false + continue + fi + case "$arg" in + --local) STUDIO_LOCAL_INSTALL=true ;; + --package) _next_is_package=true ;; + esac +done + +if [ "$_next_is_package" = true ]; then + echo "❌ ERROR: --package requires an argument." >&2 + exit 1 +fi + PYTHON_VERSION="3.13" +STUDIO_HOME="$HOME/.unsloth/studio" +VENV_DIR="$STUDIO_HOME/unsloth_studio" # ── Helper: download a URL to a file (supports curl and wget) ── download() { @@ -89,6 +113,441 @@ _smart_apt_install() { fi } +# ── Helper: create desktop shortcuts and launcher script ── +# Usage: create_studio_shortcuts +# Creates ~/.local/share/unsloth/launch-studio.sh (shared launcher), +# plus platform-specific shortcuts (Linux .desktop / macOS .app bundle). +# Skipped on WSL (no native desktop). +create_studio_shortcuts() { + _css_exe="$1" + _css_os="$2" + + # Skip on WSL -- no native desktop environment + if [ "$_css_os" = "wsl" ]; then + return 0 + fi + + # Validate exe + if [ ! -x "$_css_exe" ]; then + echo "[WARN] Cannot create shortcuts: unsloth not found at $_css_exe" + return 0 + fi + + # Resolve absolute path + _css_exe_dir=$(cd "$(dirname "$_css_exe")" && pwd) + _css_exe="$_css_exe_dir/$(basename "$_css_exe")" + + _css_data_dir="$HOME/.local/share/unsloth" + _css_launcher="$_css_data_dir/launch-studio.sh" + _css_icon_png="$_css_data_dir/unsloth-studio.png" + _css_gem_png="$_css_data_dir/unsloth-gem.png" + + mkdir -p "$_css_data_dir" + + # ── Write launcher script ── + # The launcher is Bash (not POSIX sh). + # We write it with a placeholder and substitute the exe path via sed. + cat > "$_css_launcher" << 'LAUNCHER_EOF' +#!/usr/bin/env bash +# Unsloth Studio Launcher +# Auto-generated by install.sh -- do not edit manually. +set -euo pipefail + +DATA_DIR="$HOME/.local/share/unsloth" + +# Read exe path from config written at install time. +# Sourcing is safe: the config file is written by install.sh, not user input. +if [ -f "$DATA_DIR/studio.conf" ]; then + . "$DATA_DIR/studio.conf" +fi +if [ -z "${UNSLOTH_EXE:-}" ] || [ ! -x "${UNSLOTH_EXE:-}" ]; then + echo "Error: UNSLOTH_EXE not set or not executable. Re-run the installer." >&2 + exit 1 +fi + +BASE_PORT=8888 +MAX_PORT_OFFSET=20 +TIMEOUT_SEC=60 +POLL_INTERVAL_SEC=1 +LOG_FILE="$DATA_DIR/studio.log" +LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp}/unsloth-studio-launcher-$(id -u).lock" + +# ── HTTP GET helper (supports curl and wget) ── +_http_get() { + _url="$1" + if command -v curl >/dev/null 2>&1; then + curl -fsS --max-time 1 "$_url" 2>/dev/null + elif command -v wget >/dev/null 2>&1; then + wget -qO- --timeout=1 "$_url" 2>/dev/null + else + return 1 + fi +} + +# ── Health check ── +_check_health() { + _port=$1 + _resp=$(_http_get "http://127.0.0.1:$_port/api/health") || return 1 + case "$_resp" in + *'"status"'*'"healthy"'*'"service"'*'"Unsloth UI Backend"'*) return 0 ;; + *'"service"'*'"Unsloth UI Backend"'*'"status"'*'"healthy"'*) return 0 ;; + esac + return 1 +} + +# ── Port scanning ── +_candidate_ports() { + echo "$BASE_PORT" + _max_port=$((BASE_PORT + MAX_PORT_OFFSET)) + if command -v ss >/dev/null 2>&1; then + ss -tlnH 2>/dev/null | awk '{print $4}' | grep -oE '[0-9]+$' | \ + awk -v lo="$BASE_PORT" -v hi="$_max_port" '$1 >= lo && $1 <= hi && $1 != lo {print}' || true + elif command -v lsof >/dev/null 2>&1; then + lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null | awk '{print $9}' | grep -oE '[0-9]+$' | \ + awk -v lo="$BASE_PORT" -v hi="$_max_port" '$1 >= lo && $1 <= hi && $1 != lo {print}' || true + else + _offset=1 + while [ "$_offset" -le "$MAX_PORT_OFFSET" ]; do + echo $((BASE_PORT + _offset)) + _offset=$((_offset + 1)) + done + fi +} + +_find_healthy_port() { + for _p in $(_candidate_ports | sort -un); do + if _check_health "$_p"; then + echo "$_p" + return 0 + fi + done + return 1 +} + +# ── Check if a port is busy ── +_is_port_busy() { + _port=$1 + if command -v ss >/dev/null 2>&1; then + ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE "[.:]$_port$" + elif command -v lsof >/dev/null 2>&1; then + lsof -iTCP:"$_port" -sTCP:LISTEN -nP >/dev/null 2>&1 + else + return 1 + fi +} + +# ── Find a free port in range ── +_find_launch_port() { + _offset=0 + while [ "$_offset" -le "$MAX_PORT_OFFSET" ]; do + _candidate=$((BASE_PORT + _offset)) + if ! _is_port_busy "$_candidate"; then + echo "$_candidate" + return 0 + fi + _offset=$((_offset + 1)) + done + return 1 +} + +# ── Open browser ── +_open_browser() { + _url="$1" + if [ "$(uname)" = "Darwin" ] && command -v open >/dev/null 2>&1; then + open "$_url" + elif command -v xdg-open >/dev/null 2>&1; then + xdg-open "$_url" >/dev/null 2>&1 & + else + echo "Open in your browser: $_url" >&2 + fi +} + +# ── Spawn terminal with studio command ── +_spawn_terminal() { + _cmd="$1" + _os=$(uname) + if [ "$_os" = "Darwin" ]; then + # Escape backslashes and double-quotes for AppleScript string + _cmd_escaped=$(printf '%s' "$_cmd" | sed 's/\\/\\\\/g; s/"/\\"/g') + osascript -e "tell application \"Terminal\" to do script \"$_cmd_escaped\"" >/dev/null 2>&1 && return 0 + else + for _term in gnome-terminal konsole xfce4-terminal mate-terminal lxterminal xterm; do + if command -v "$_term" >/dev/null 2>&1; then + case "$_term" in + gnome-terminal) "$_term" -- sh -c "$_cmd" & return 0 ;; + konsole) "$_term" -e sh -c "$_cmd" & return 0 ;; + xterm) "$_term" -e sh -c "$_cmd" & return 0 ;; + *) "$_term" -e sh -c "$_cmd" & return 0 ;; + esac + fi + done + fi + # Fallback: background with log + echo "No terminal emulator found; running in background. Logs: $LOG_FILE" >&2 + nohup sh -c "$_cmd" >> "$LOG_FILE" 2>&1 & + return 0 +} + +# ── Atomic directory-based single-instance guard ── +_acquire_lock() { + if mkdir "$LOCK_DIR" 2>/dev/null; then + echo "$$" > "$LOCK_DIR/pid" + return 0 + fi + + # Lock dir exists -- check if owner is still alive + _old_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null || true) + if [ -n "$_old_pid" ] && kill -0 "$_old_pid" 2>/dev/null; then + # Another launcher is running; wait for it to bring Studio up + _deadline=$(($(date +%s) + TIMEOUT_SEC)) + while [ "$(date +%s)" -lt "$_deadline" ]; do + _port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 + } + sleep "$POLL_INTERVAL_SEC" + done + echo "Timed out waiting for other launcher (PID $_old_pid)" >&2 + exit 0 + fi + + # Stale lock -- reclaim + rm -rf "$LOCK_DIR" + mkdir "$LOCK_DIR" 2>/dev/null || return 1 + echo "$$" > "$LOCK_DIR/pid" +} + +_release_lock() { + rm -rf "$LOCK_DIR" +} + +# ── Main ── +# Fast path: already healthy +_port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 +} + +_acquire_lock +trap '_release_lock' EXIT INT TERM + +# Post-lock re-check (handles race with another launcher) +_port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 +} + +# Find a free port in range +_launch_port=$(_find_launch_port) || { + echo "No free port found in range ${BASE_PORT}-$((BASE_PORT + MAX_PORT_OFFSET))" >&2 + exit 1 +} + +# Launch studio in a terminal +_launch_cmd=$(printf '%q ' "$UNSLOTH_EXE" studio -H 0.0.0.0 -p "$_launch_port") +_launch_cmd=${_launch_cmd% } +_spawn_terminal "$_launch_cmd" + +# Poll for health +_deadline=$(($(date +%s) + TIMEOUT_SEC)) +while [ "$(date +%s)" -lt "$_deadline" ]; do + _port=$(_find_healthy_port) && { + _open_browser "http://localhost:$_port" + exit 0 + } + sleep "$POLL_INTERVAL_SEC" +done + +echo "Unsloth Studio did not become healthy within ${TIMEOUT_SEC}s." >&2 +echo "Check logs at: $LOG_FILE" >&2 +exit 1 +LAUNCHER_EOF + + chmod +x "$_css_launcher" + + # Write the exe path to a separate conf file sourced by the launcher. + # Using single-quote wrapping with the standard '\'' escape for any + # embedded apostrophes. This avoids all sed metacharacter issues. + _css_quoted_exe=$(printf '%s' "$_css_exe" | sed "s/'/'\\\\''/g") + printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'" > "$_css_data_dir/studio.conf" + + # ── Icon: try bundled, then download ── + # favicon.png (small, for Linux) and unsloth-gem.png (large, for macOS icns) + _css_script_dir="" + if [ -n "${0:-}" ] && [ -f "$0" ]; then + _css_script_dir=$(cd "$(dirname "$0")" 2>/dev/null && pwd) || true + fi + + # Try to find favicon.png from installed package (site-packages) or local repo + _css_found_favicon="" + _css_found_gem="" + _css_venv_dir=$(dirname "$(dirname "$_css_exe")") + # Check site-packages + for _sp in "$_css_venv_dir"/lib/python*/site-packages/unsloth/studio/frontend/public; do + if [ -f "$_sp/favicon.png" ]; then + _css_found_favicon="$_sp/favicon.png" + fi + if [ -f "$_sp/unsloth-gem.png" ]; then + _css_found_gem="$_sp/unsloth-gem.png" + fi + done + # Check local repo (when running from clone) + if [ -z "$_css_found_favicon" ] && [ -n "$_css_script_dir" ] && [ -f "$_css_script_dir/studio/frontend/public/favicon.png" ]; then + _css_found_favicon="$_css_script_dir/studio/frontend/public/favicon.png" + fi + if [ -z "$_css_found_gem" ] && [ -n "$_css_script_dir" ] && [ -f "$_css_script_dir/studio/frontend/public/unsloth-gem.png" ]; then + _css_found_gem="$_css_script_dir/studio/frontend/public/unsloth-gem.png" + fi + + # Copy or download favicon.png + if [ -n "$_css_found_favicon" ]; then + cp "$_css_found_favicon" "$_css_icon_png" 2>/dev/null || true + elif [ ! -f "$_css_icon_png" ]; then + download "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/favicon.png" "$_css_icon_png" 2>/dev/null || true + fi + # Copy or download unsloth-gem.png (for macOS icns) + if [ -n "$_css_found_gem" ]; then + cp "$_css_found_gem" "$_css_gem_png" 2>/dev/null || true + elif [ ! -f "$_css_gem_png" ]; then + download "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth-gem.png" "$_css_gem_png" 2>/dev/null || true + fi + + # Validate PNG header (first 4 bytes: \x89PNG) + _css_validate_png() { + [ -f "$1" ] || return 1 + _hdr=$(od -An -tx1 -N4 "$1" 2>/dev/null | tr -d ' ') + [ "$_hdr" = "89504e47" ] + } + if [ -f "$_css_icon_png" ] && ! _css_validate_png "$_css_icon_png"; then + rm -f "$_css_icon_png" + fi + if [ -f "$_css_gem_png" ] && ! _css_validate_png "$_css_gem_png"; then + rm -f "$_css_gem_png" + fi + + # ── Platform-specific shortcuts ── + _css_created=0 + + if [ "$_css_os" = "linux" ]; then + # ── Linux: .desktop file ── + _css_app_dir="$HOME/.local/share/applications" + mkdir -p "$_css_app_dir" + + _css_desktop="$_css_app_dir/unsloth-studio.desktop" + # Escape backslashes and double-quotes for .desktop Exec= field + _css_exec_escaped=$(printf '%s' "$_css_launcher" | sed 's/\\/\\\\/g; s/"/\\"/g') + _css_icon_escaped=$(printf '%s' "$_css_icon_png" | sed 's/\\/\\\\/g; s/"/\\"/g') + cat > "$_css_desktop" << DESKTOP_EOF +[Desktop Entry] +Version=1.0 +Type=Application +Name=Unsloth Studio +Comment=Launch Unsloth Studio +Exec="$_css_exec_escaped" +Icon=$_css_icon_escaped +Terminal=false +StartupNotify=true +Categories=Development;Science; +DESKTOP_EOF + chmod +x "$_css_desktop" + + # Copy to ~/Desktop if it exists + if [ -d "$HOME/Desktop" ]; then + cp "$_css_desktop" "$HOME/Desktop/unsloth-studio.desktop" 2>/dev/null || true + chmod +x "$HOME/Desktop/unsloth-studio.desktop" 2>/dev/null || true + # Mark as trusted so GNOME/Nautilus allows launching via double-click + if command -v gio >/dev/null 2>&1; then + gio set "$HOME/Desktop/unsloth-studio.desktop" metadata::trusted true 2>/dev/null || true + fi + fi + + # Best-effort update database + update-desktop-database "$_css_app_dir" 2>/dev/null || true + _css_created=1 + + elif [ "$_css_os" = "macos" ]; then + # ── macOS: .app bundle ── + _css_app="$HOME/Applications/Unsloth Studio.app" + _css_contents="$_css_app/Contents" + _css_macos_dir="$_css_contents/MacOS" + _css_res_dir="$_css_contents/Resources" + mkdir -p "$_css_macos_dir" "$_css_res_dir" + + # Info.plist + cat > "$_css_contents/Info.plist" << 'PLIST_EOF' + + + + + CFBundleIdentifier + ai.unsloth.studio + CFBundleName + Unsloth Studio + CFBundleDisplayName + Unsloth Studio + CFBundleExecutable + launch-studio + CFBundleIconFile + AppIcon + CFBundlePackageType + APPL + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + LSMinimumSystemVersion + 10.15 + NSHighResolutionCapable + + + +PLIST_EOF + + # Executable stub + cat > "$_css_macos_dir/launch-studio" << STUB_EOF +#!/bin/sh +exec "$HOME/.local/share/unsloth/launch-studio.sh" "\$@" +STUB_EOF + chmod +x "$_css_macos_dir/launch-studio" + + # Build AppIcon.icns from unsloth-gem.png (2240x2240) + if [ -f "$_css_gem_png" ] && command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then + _css_tmpdir=$(mktemp -d 2>/dev/null) + if [ -d "$_css_tmpdir" ]; then + _css_iconset="$_css_tmpdir/AppIcon.iconset" + mkdir -p "$_css_iconset" + _css_icon_ok=true + for _sz in 16 32 128 256 512; do + _sz2=$((_sz * 2)) + sips -z "$_sz" "$_sz" "$_css_gem_png" --out "$_css_iconset/icon_${_sz}x${_sz}.png" >/dev/null 2>&1 || _css_icon_ok=false + sips -z "$_sz2" "$_sz2" "$_css_gem_png" --out "$_css_iconset/icon_${_sz}x${_sz}@2x.png" >/dev/null 2>&1 || _css_icon_ok=false + done + if [ "$_css_icon_ok" = "true" ]; then + iconutil -c icns "$_css_iconset" -o "$_css_res_dir/AppIcon.icns" 2>/dev/null || true + fi + rm -rf "$_css_tmpdir" + fi + fi + # Fallback: copy PNG as icon + if [ ! -f "$_css_res_dir/AppIcon.icns" ] && [ -f "$_css_icon_png" ]; then + cp "$_css_icon_png" "$_css_res_dir/AppIcon.icns" 2>/dev/null || true + fi + + # Touch so Finder indexes it + touch "$_css_app" + + # Symlink on Desktop + if [ -d "$HOME/Desktop" ]; then + ln -sf "$_css_app" "$HOME/Desktop/Unsloth Studio" 2>/dev/null || true + fi + _css_created=1 + fi + + if [ "$_css_created" -eq 1 ]; then + echo "[OK] Created Unsloth Studio shortcut(s)" + fi +} + echo "" echo "=========================================" echo " Unsloth Studio Installer" @@ -224,32 +683,197 @@ if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then export PATH="$HOME/.local/bin:$PATH" fi -# ── Create venv (skip if it already exists and has a valid interpreter) ── -if [ ! -x "$VENV_NAME/bin/python" ]; then - [ -e "$VENV_NAME" ] && rm -rf "$VENV_NAME" - echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_NAME})..." - uv venv "$VENV_NAME" --python "$PYTHON_VERSION" -else - echo "==> Virtual environment ${VENV_NAME} already exists, skipping creation." +# ── Create venv (migrate old layout if possible, otherwise fresh) ── +mkdir -p "$STUDIO_HOME" + +_MIGRATED=false + +if [ -x "$VENV_DIR/bin/python" ]; then + # New layout already exists — nuke for fresh install + rm -rf "$VENV_DIR" +elif [ -x "$STUDIO_HOME/.venv/bin/python" ]; then + # Old layout exists — validate before migrating + echo "==> Found legacy Studio environment, validating..." + if "$STUDIO_HOME/.venv/bin/python" -c " +import torch +device = 'cuda' if torch.cuda.is_available() else 'cpu' +A = torch.ones((10, 10), device=device) +B = torch.ones((10, 10), device=device) +C = torch.ones((10, 10), device=device) +D = A + B +E = D @ C +torch.testing.assert_close(torch.unique(E), torch.tensor((20,), device=E.device, dtype=E.dtype)) +" >/dev/null 2>&1; then + echo "✅ Legacy environment is healthy — migrating..." + mv "$STUDIO_HOME/.venv" "$VENV_DIR" + echo " Moved ~/.unsloth/studio/.venv → $VENV_DIR" + _MIGRATED=true + else + echo "⚠️ Legacy environment failed validation — creating fresh environment" + rm -rf "$STUDIO_HOME/.venv" + fi fi +if [ ! -x "$VENV_DIR/bin/python" ]; then + echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_DIR})..." + uv venv "$VENV_DIR" --python "$PYTHON_VERSION" +else + echo "==> Using migrated environment at ${VENV_DIR}" +fi + +# ── Resolve repo root (for --local installs) ── +_REPO_ROOT="$(cd "$(dirname "$0" 2>/dev/null || echo ".")" && pwd)" + +# ── Detect GPU and choose PyTorch index URL ── +# Mirrors Get-TorchIndexUrl in install.ps1. +# On CPU-only machines this returns the cpu index, avoiding the solver +# dead-end where --torch-backend=auto resolves to unsloth==2024.8. +get_torch_index_url() { + _base="https://download.pytorch.org/whl" + # macOS: always CPU (no CUDA support) + case "$(uname -s)" in Darwin) echo "$_base/cpu"; return ;; esac + # Try nvidia-smi + _smi="" + if command -v nvidia-smi >/dev/null 2>&1; then + _smi="nvidia-smi" + elif [ -x "/usr/bin/nvidia-smi" ]; then + _smi="/usr/bin/nvidia-smi" + fi + if [ -z "$_smi" ]; then echo "$_base/cpu"; return; fi + # Parse CUDA version from nvidia-smi output (POSIX-safe, no grep -P) + _cuda_ver=$(LC_ALL=C $_smi 2>/dev/null \ + | sed -n 's/.*CUDA Version:[[:space:]]*\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \ + | head -1) + if [ -z "$_cuda_ver" ]; then + echo "[WARN] Could not determine CUDA version from nvidia-smi, defaulting to cu126" >&2 + echo "$_base/cu126"; return + fi + _major=${_cuda_ver%%.*} + _minor=${_cuda_ver#*.} + if [ "$_major" -ge 13 ]; then echo "$_base/cu130" + elif [ "$_major" -eq 12 ] && [ "$_minor" -ge 8 ]; then echo "$_base/cu128" + elif [ "$_major" -eq 12 ] && [ "$_minor" -ge 6 ]; then echo "$_base/cu126" + elif [ "$_major" -ge 12 ]; then echo "$_base/cu124" + elif [ "$_major" -ge 11 ]; then echo "$_base/cu118" + else echo "$_base/cpu"; fi +} +TORCH_INDEX_URL=$(get_torch_index_url) + # ── Install unsloth directly into the venv (no activation needed) ── -echo "==> Installing unsloth (this may take a few minutes)..." -uv pip install --python "$VENV_NAME/bin/python" unsloth --torch-backend=auto +_VENV_PY="$VENV_DIR/bin/python" +if [ "$_MIGRATED" = true ]; then + # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state + # in the new venv location, while preserving existing torch/CUDA + echo "==> Upgrading unsloth in migrated environment..." + uv pip install --python "$_VENV_PY" \ + --reinstall-package unsloth --reinstall-package unsloth-zoo \ + "unsloth>=2026.3.11" unsloth-zoo + if [ "$STUDIO_LOCAL_INSTALL" = true ]; then + echo "==> Overlaying local repo (editable)..." + uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps + fi +elif [ -n "$TORCH_INDEX_URL" ]; then + # Fresh: Step 1 - install torch from explicit index + echo "==> Installing PyTorch ($TORCH_INDEX_URL)..." + uv pip install --python "$_VENV_PY" "torch>=2.4,<2.11.0" torchvision torchaudio \ + --index-url "$TORCH_INDEX_URL" + # Fresh: Step 2 - install unsloth, preserving pre-installed torch + echo "==> Installing unsloth (this may take a few minutes)..." + if [ "$STUDIO_LOCAL_INSTALL" = true ]; then + uv pip install --python "$_VENV_PY" \ + --upgrade-package unsloth "unsloth>=2026.3.11" unsloth-zoo + echo "==> Overlaying local repo (editable)..." + uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps + else + uv pip install --python "$_VENV_PY" \ + --upgrade-package unsloth "$PACKAGE_NAME" + fi +else + # Fallback: GPU detection failed to produce a URL -- let uv resolve torch + echo "==> Installing unsloth (this may take a few minutes)..." + if [ "$STUDIO_LOCAL_INSTALL" = true ]; then + uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.3.11" --torch-backend=auto + echo "==> Overlaying local repo (editable)..." + uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps + else + uv pip install --python "$_VENV_PY" "$PACKAGE_NAME" --torch-backend=auto + fi +fi # ── Run studio setup ── -# Ensure the venv's Python is on PATH for setup.sh's Python discovery. -# On macOS the system Python may be outside the 3.11-3.13 range that -# setup.sh requires, but uv already installed a compatible interpreter -# inside the venv. -VENV_ABS_BIN="$(cd "$VENV_NAME/bin" && pwd)" +# When --local, use the repo's own setup.sh directly. +# Otherwise, find it inside the installed package. +SETUP_SH="" +if [ "$STUDIO_LOCAL_INSTALL" = true ] && [ -f "$_REPO_ROOT/studio/setup.sh" ]; then + SETUP_SH="$_REPO_ROOT/studio/setup.sh" +fi + +if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then + SETUP_SH=$("$VENV_DIR/bin/python" -c " +import importlib.resources +print(importlib.resources.files('studio') / 'setup.sh') +" 2>/dev/null || echo "") +fi + +# Fallback: search site-packages +if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then + SETUP_SH=$(find "$VENV_DIR" -path "*/studio/setup.sh" -print -quit 2>/dev/null || echo "") +fi + +if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then + echo "❌ ERROR: Could not find studio/setup.sh in the installed package." + exit 1 +fi + +# Ensure the venv's Python is on PATH so setup.sh can find it. +VENV_ABS_BIN="$(cd "$VENV_DIR/bin" && pwd)" if [ -n "$VENV_ABS_BIN" ]; then export PATH="$VENV_ABS_BIN:$PATH" fi -echo "==> Running unsloth studio setup..." -REQUESTED_PYTHON_VERSION="$(cd "$VENV_NAME/bin" && pwd)/python" \ -"$VENV_NAME/bin/unsloth" studio setup Running unsloth setup..." +if [ "$STUDIO_LOCAL_INSTALL" = true ]; then + SKIP_STUDIO_BASE=1 \ + STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \ + STUDIO_LOCAL_INSTALL=1 \ + STUDIO_LOCAL_REPO="$_REPO_ROOT" \ + bash "$SETUP_SH" /dev/null; then + echo '' >> "$_SHELL_PROFILE" + echo '# Added by Unsloth installer' >> "$_SHELL_PROFILE" + echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$_SHELL_PROFILE" + echo "==> Added ~/.local/bin to PATH in $_SHELL_PROFILE" + fi + fi + export PATH="$_LOCAL_BIN:$PATH" + ;; +esac + +create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" echo "" echo "=========================================" @@ -257,8 +881,32 @@ echo " Unsloth Studio installed!" echo "=========================================" echo "" -echo " To launch, run:" -echo "" -echo " source ${VENV_NAME}/bin/activate" -echo " unsloth studio -H 0.0.0.0 -p 8888" -echo "" +# Launch studio automatically in interactive terminals; +# in non-interactive environments (Docker, CI, cloud-init) just print instructions. +if [ -t 1 ]; then + echo "==> Launching Unsloth Studio..." + echo "" + "$VENV_DIR/bin/unsloth" studio -H 0.0.0.0 -p 8888 + _LAUNCH_EXIT=$? + if [ "$_LAUNCH_EXIT" -ne 0 ] && [ "$_MIGRATED" = true ]; then + echo "" + echo "⚠️ Unsloth Studio failed to start after migration." + echo " Your migrated environment may be incompatible." + echo " To fix, remove the environment and reinstall:" + echo "" + echo " rm -rf $VENV_DIR" + echo " curl -fsSL https://unsloth.ai/install.sh | sh" + echo "" + fi + exit "$_LAUNCH_EXIT" +else + echo " To launch, run:" + echo "" + echo " unsloth studio -H 0.0.0.0 -p 8888" + echo "" + echo " Or activate the environment first:" + echo "" + echo " source ${VENV_DIR}/bin/activate" + echo " unsloth studio -H 0.0.0.0 -p 8888" + echo "" +fi diff --git a/pyproject.toml b/pyproject.toml index eaa7d84f71..afa96bbbcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,12 +69,9 @@ studio = [ "*.ps1", "*.bat", "frontend/dist/**/*", - "frontend/public/**/*", - "frontend/src/**/*", "frontend/*.json", "frontend/*.ts", "frontend/*.js", - "frontend/*.lock", "frontend/*.html", "frontend/*.yaml", "frontend/.git*", diff --git a/studio/backend/colab.py b/studio/backend/colab.py index 25a9408ccb..ecf9fc2907 100644 --- a/studio/backend/colab.py +++ b/studio/backend/colab.py @@ -26,7 +26,7 @@ def _bootstrap_studio_venv() -> None: site-packages so that packages like structlog, fastapi, etc. are importable from notebook cells and take priority over system copies. """ - venv_lib = Path.home() / ".unsloth" / "studio" / ".venv" / "lib" + venv_lib = Path.home() / ".unsloth" / "studio" / "unsloth_studio" / "lib" if not venv_lib.exists(): import warnings diff --git a/studio/backend/core/data_recipe/oxc-validator/package.json b/studio/backend/core/data_recipe/oxc-validator/package.json index a47c0ea521..d1c765a2e1 100644 --- a/studio/backend/core/data_recipe/oxc-validator/package.json +++ b/studio/backend/core/data_recipe/oxc-validator/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "type": "module", "dependencies": { - "oxc-parser": "^0.116.0", + "oxc-parser": "^0.121.0", "oxlint": "^1.51.0" } } diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 7b1db8fd04..1d5643ac09 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -857,6 +857,9 @@ class LlamaCppBackend: if use_fit: cmd.extend(["--fit", "on"]) + elif gpu_indices is not None: + # Model fits on selected GPU(s) -- offload all layers + cmd.extend(["-ngl", "-1"]) if n_threads is not None: cmd.extend(["--threads", str(n_threads)]) @@ -966,6 +969,46 @@ class LlamaCppBackend: lib_dirs = [binary_dir] _arch = platform.machine() # x86_64, aarch64, etc. + + # Pip-installed nvidia CUDA runtime libs (e.g. torch's + # bundled cuda-bindings). The prebuilt llama.cpp binary + # links against libcudart.so.13 / libcublas.so.13 which + # live here, not in /usr/local/cuda. + import glob as _glob + + for _nv_pattern in [ + os.path.join( + sys.prefix, + "lib", + "python*", + "site-packages", + "nvidia", + "cu*", + "lib", + ), + os.path.join( + sys.prefix, + "lib", + "python*", + "site-packages", + "nvidia", + "cudnn", + "lib", + ), + os.path.join( + sys.prefix, + "lib", + "python*", + "site-packages", + "nvidia", + "nvjitlink", + "lib", + ), + ]: + for _nv_dir in _glob.glob(_nv_pattern): + if os.path.isdir(_nv_dir): + lib_dirs.append(_nv_dir) + for cuda_lib in [ "/usr/local/cuda/lib64", f"/usr/local/cuda/targets/{_arch}-linux/lib", diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 5f504bbdf4..2324916236 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -81,8 +81,8 @@ class TrainingProgress: epoch: float = 0 step: int = 0 total_steps: int = 0 - loss: float = 0.0 - learning_rate: float = 0.0 + loss: Optional[float] = None + learning_rate: Optional[float] = None is_training: bool = False is_completed: bool = False error: Optional[str] = None @@ -244,7 +244,7 @@ class UnslothTrainer: def on_log(self, args, state, control, logs = None, **kwargs): if not logs: return - loss_value = logs.get("loss", logs.get("train_loss", 0.0)) + loss_value = logs.get("loss", logs.get("train_loss", None)) current_step = state.global_step grad_norm = logs.get("grad_norm", None) @@ -268,7 +268,7 @@ class UnslothTrainer: step = current_step, epoch = round(state.epoch, 2) if state.epoch else 0, loss = loss_value, - learning_rate = logs.get("learning_rate", 0.0), + learning_rate = logs.get("learning_rate", None), elapsed_seconds = elapsed_seconds, eta_seconds = eta_seconds, grad_norm = grad_norm, diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 9626f9df2e..4439e4e173 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -14,12 +14,14 @@ worker's mp.Queue, and exposes the same API surface to routes/training.py. Pattern follows core/data_recipe/jobs/manager.py. """ +import json as _json import math import multiprocessing as mp import queue import threading import time import structlog +from datetime import datetime, timezone from loggers import get_logger from dataclasses import dataclass, field from pathlib import Path @@ -44,8 +46,8 @@ class TrainingProgress: epoch: float = 0 step: int = 0 total_steps: int = 0 - loss: float = 0.0 - learning_rate: float = 0.0 + loss: Optional[float] = None + learning_rate: Optional[float] = None is_training: bool = False is_completed: bool = False error: Optional[str] = None @@ -63,6 +65,8 @@ class TrainingBackend: Launches a fresh subprocess per training job, communicates via mp.Queue. """ + FLUSH_THRESHOLD: int = 10 + def __init__(self): # Subprocess state self._proc: Optional[mp.Process] = None @@ -91,13 +95,21 @@ class TrainingBackend: self.current_job_id: Optional[str] = None self._output_dir: Optional[str] = None + # DB persistence + self._metric_buffer: list[dict] = [] + self._run_finalized: bool = False + self._db_run_created: bool = False + self._db_total_steps_set: bool = False + self._db_config: Optional[dict] = None + self._db_started_at: Optional[str] = None + logger.info("TrainingBackend initialized (subprocess mode)") # ------------------------------------------------------------------ # Public API (called by routes/training.py) # ------------------------------------------------------------------ - def start_training(self, **kwargs) -> bool: + def start_training(self, job_id: str, **kwargs) -> bool: """Spawn a subprocess to run the full training pipeline. All kwargs are serialized into a config dict and sent to the worker. @@ -108,30 +120,16 @@ class TrainingBackend: logger.warning("Training subprocess already running") return False - # Join prior pump thread to prevent it from consuming events - # from the new job's queue (it reads self._event_queue dynamically). + # Join prior pump thread — refuse to start if it won't die if self._pump_thread is not None and self._pump_thread.is_alive(): self._pump_thread.join(timeout = 5.0) if self._pump_thread.is_alive(): - logger.warning("Previous pump thread did not exit within 5s") + logger.warning( + "Previous pump thread did not exit within 5s — refusing to start" + ) + return False self._pump_thread = None - # Reset state - self._should_stop = False - self._cancel_requested = False - self._progress = TrainingProgress( - is_training = True, status_message = "Initializing training..." - ) - self.loss_history.clear() - self.lr_history.clear() - self.step_history.clear() - self.grad_norm_history.clear() - self.grad_norm_step_history.clear() - self.eval_loss_history.clear() - self.eval_step_history.clear() - self.eval_enabled = False - self._output_dir = None - # Build config dict for the subprocess config = { "model_name": kwargs["model_name"], @@ -193,23 +191,62 @@ class TrainingBackend: if config["training_type"] != "LoRA/QLoRA": config["load_in_4bit"] = False - # Spawn subprocess + # Spawn subprocess — use locals so state is untouched on failure from .worker import run_training_process - self._event_queue = _CTX.Queue() - self._stop_queue = _CTX.Queue() + event_queue = _CTX.Queue() + stop_queue = _CTX.Queue() - self._proc = _CTX.Process( + proc = _CTX.Process( target = run_training_process, kwargs = { - "event_queue": self._event_queue, - "stop_queue": self._stop_queue, + "event_queue": event_queue, + "stop_queue": stop_queue, "config": config, }, daemon = True, ) - self._proc.start() - logger.info("Training subprocess started (pid=%s)", self._proc.pid) + try: + proc.start() + except Exception: + logger.error("Failed to start training subprocess", exc_info = True) + return False + + logger.info("Training subprocess started (pid=%s)", proc.pid) + + # Reset state — safe because old pump thread is confirmed dead + # and proc.start() succeeded + self.current_job_id = job_id + self._should_stop = False + self._cancel_requested = False + self._progress = TrainingProgress( + is_training = True, status_message = "Initializing training..." + ) + self.loss_history.clear() + self.lr_history.clear() + self.step_history.clear() + self.grad_norm_history.clear() + self.grad_norm_step_history.clear() + self.eval_loss_history.clear() + self.eval_step_history.clear() + self.eval_enabled = False + self._output_dir = None + self._metric_buffer.clear() + self._run_finalized = False + self._db_run_created = False + self._db_total_steps_set = False + self._db_config = { + k: v for k, v in config.items() if k not in {"hf_token", "wandb_token"} + } + self._db_started_at = datetime.now(timezone.utc).isoformat() + + # Assign subprocess handles after state reset + self._event_queue = event_queue + self._stop_queue = stop_queue + self._proc = proc + + # Eagerly create DB run row so the run appears in history during model loading + self._ensure_db_run_created() # Start event pump thread self._pump_thread = threading.Thread(target = self._pump_loop, daemon = True) @@ -252,6 +289,11 @@ class TrainingBackend: proc.kill() proc.join(timeout = 2.0) + # Wait for pump thread to finish DB finalization before returning + # (8s covers SQLite's default 5s lock timeout plus execution overhead) + if self._pump_thread is not None and self._pump_thread.is_alive(): + self._pump_thread.join(timeout = 8.0) + def is_training_active(self) -> bool: """Check if training is currently active.""" with self._lock: @@ -389,20 +431,54 @@ class TrainingBackend: self._progress.error or "Training process exited unexpectedly" ) + + self._ensure_db_run_created() + self._finalize_run_in_db( + status = "stopped" if self._should_stop else "error", + error_message = None + if self._should_stop + else "Training process terminated unexpectedly", + ) return def _handle_event(self, event: dict) -> None: - """Apply a subprocess event to local state.""" + """Apply a subprocess event to local state. + + State updates happen inside self._lock; DB I/O happens after + releasing it so status-polling API endpoints are never blocked + by slow SQLite writes. + """ etype = event.get("type") + db_action: Optional[str] = None + db_action_kwargs: dict = {} with self._lock: if etype == "progress": self._progress.step = event.get("step", self._progress.step) self._progress.epoch = event.get("epoch", self._progress.epoch) - self._progress.loss = event.get("loss", self._progress.loss) - self._progress.learning_rate = event.get( - "learning_rate", self._progress.learning_rate - ) + # loss/lr are sanitized below; update progress after coercion + _raw_loss = event.get("loss") + _raw_lr = event.get("learning_rate") + try: + _safe_loss = float(_raw_loss) if _raw_loss is not None else None + except (TypeError, ValueError): + logger.debug("Could not convert loss to float: %s", _raw_loss) + _safe_loss = None + if _safe_loss is not None and not math.isfinite(_safe_loss): + _safe_loss = None + try: + _safe_lr = float(_raw_lr) if _raw_lr is not None else None + except (TypeError, ValueError): + logger.debug( + "Could not convert learning_rate to float: %s", _raw_lr + ) + _safe_lr = None + if _safe_lr is not None and not math.isfinite(_safe_lr): + _safe_lr = None + if _safe_loss is not None: + self._progress.loss = _safe_loss + if _safe_lr is not None: + self._progress.learning_rate = _safe_lr self._progress.total_steps = event.get( "total_steps", self._progress.total_steps ) @@ -416,30 +492,85 @@ class TrainingBackend: if status: self._progress.status_message = status - # Update metric histories + # Update metric histories — reuse sanitized values from above step = event.get("step", 0) - loss = event.get("loss", 0.0) - lr = event.get("learning_rate", 0.0) - if step >= 0 and loss > 0: + loss = _safe_loss + lr = _safe_lr + if step > 0 and loss is not None: self.loss_history.append(loss) - self.lr_history.append(lr) + self.lr_history.append(lr if lr is not None else 0.0) self.step_history.append(step) grad_norm = event.get("grad_norm") + gn = None if grad_norm is not None: try: gn = float(grad_norm) except (TypeError, ValueError): gn = None - if gn is not None and math.isfinite(gn): + if step > 0 and gn is not None and math.isfinite(gn): self.grad_norm_history.append(gn) self.grad_norm_step_history.append(step) + else: + gn = None eval_loss = event.get("eval_loss") if eval_loss is not None: - self.eval_loss_history.append(eval_loss) - self.eval_step_history.append(step) - self.eval_enabled = True + try: + eval_loss = float(eval_loss) + except (TypeError, ValueError): + logger.debug( + "Could not convert eval_loss to float: %s", eval_loss + ) + eval_loss = None + if step > 0 and eval_loss is not None and math.isfinite(eval_loss): + self.eval_loss_history.append(eval_loss) + self.eval_step_history.append(step) + self.eval_enabled = True + else: + eval_loss = None + + # Buffer metric for DB flush (loss/lr already sanitized above) + self._metric_buffer.append( + { + "step": step, + "loss": loss, + "learning_rate": lr, + "grad_norm": gn, + "eval_loss": eval_loss, + "epoch": event.get("epoch"), + "num_tokens": event.get("num_tokens"), + "elapsed_seconds": event.get("elapsed_seconds"), + } + ) + + # Decide which DB action to take after releasing the lock + if not self._db_run_created and self.current_job_id and self._db_config: + db_action = "create_run" + db_action_kwargs = { + "job_id": self.current_job_id, + "model_name": self._db_config["model_name"], + "dataset_name": self._db_config.get("hf_dataset") + or next( + iter(self._db_config.get("local_datasets") or []), "unknown" + ), + "config_json": _json.dumps(self._db_config), + "started_at": self._db_started_at + or datetime.now(timezone.utc).isoformat(), + "total_steps": event.get("total_steps"), + } + elif ( + event.get("total_steps") + and self._db_run_created + and not self._db_total_steps_set + ): + db_action = "update_total_steps" + db_action_kwargs = { + "job_id": self.current_job_id, + "total_steps": event["total_steps"], + } + elif len(self._metric_buffer) >= self.FLUSH_THRESHOLD: + db_action = "flush" elif etype == "eval_configured": self.eval_enabled = True @@ -454,6 +585,14 @@ class TrainingBackend: self._output_dir = event.get("output_dir") msg = event.get("status_message", "Training completed") self._progress.status_message = msg + if not self._db_run_created and self.current_job_id and self._db_config: + db_action = "create_and_finalize" + else: + db_action = "finalize" + db_action_kwargs = { + "status": "stopped" if self._should_stop else "completed", + "output_dir": self._output_dir, + } elif etype == "error": self._progress.is_training = False @@ -462,6 +601,149 @@ class TrainingBackend: stack = event.get("stack", "") if stack: logger.error("Stack trace:\n%s", stack) + if not self._db_run_created and self.current_job_id and self._db_config: + db_action = "create_and_finalize" + else: + db_action = "finalize" + db_action_kwargs = { + "status": "stopped" if self._should_stop else "error", + "error_message": event.get("error", "Unknown error"), + } + + # --- DB I/O outside the lock --- + if db_action == "create_run": + try: + from storage.studio_db import create_run + + create_run( + id = db_action_kwargs["job_id"], + model_name = db_action_kwargs["model_name"], + dataset_name = db_action_kwargs["dataset_name"], + config_json = db_action_kwargs["config_json"], + started_at = db_action_kwargs["started_at"], + total_steps = db_action_kwargs["total_steps"], + ) + self._db_run_created = True + if db_action_kwargs["total_steps"]: + self._db_total_steps_set = True + except Exception: + logger.warning("Failed to create DB run record", exc_info = True) + elif db_action == "create_and_finalize": + self._ensure_db_run_created() + self._finalize_run_in_db(**db_action_kwargs) + elif db_action == "update_total_steps": + try: + from storage.studio_db import update_run_total_steps + + update_run_total_steps( + db_action_kwargs["job_id"], db_action_kwargs["total_steps"] + ) + self._db_total_steps_set = True + except Exception: + logger.warning("Failed to update total_steps in DB", exc_info = True) + elif db_action == "flush": + self._flush_metrics_to_db() + elif db_action == "finalize": + self._finalize_run_in_db(**db_action_kwargs) + + def _ensure_db_run_created(self) -> None: + """Create the DB row if it doesn't exist yet. Called outside the lock.""" + if self._db_run_created or not self.current_job_id or not self._db_config: + return + try: + from storage.studio_db import create_run + + dataset_name = self._db_config.get("hf_dataset") or next( + iter(self._db_config.get("local_datasets") or []), "unknown" + ) + create_run( + id = self.current_job_id, + model_name = self._db_config["model_name"], + dataset_name = dataset_name, + config_json = _json.dumps(self._db_config), + started_at = self._db_started_at + or datetime.now(timezone.utc).isoformat(), + total_steps = self._progress.total_steps or None, + ) + self._db_run_created = True + except Exception: + logger.warning( + "Failed to create DB run record for early failure", exc_info = True + ) + + def _finalize_run_in_db( + self, + status: str, + error_message: Optional[str] = None, + output_dir: Optional[str] = None, + ) -> None: + """Flush remaining metrics and mark a run as finished in the DB.""" + if not self.current_job_id or not self._db_run_created or self._run_finalized: + return + self._flush_metrics_to_db() + try: + from storage.studio_db import finish_run + from utils.downsample import downsample + + sparkline = downsample(self.loss_history, 50) + finish_run( + id = self.current_job_id, + status = status, + ended_at = datetime.now(timezone.utc).isoformat(), + final_step = self._progress.step, + final_loss = self._progress.loss + if ( + self._progress.loss is not None + and math.isfinite(self._progress.loss) + ) + else None, + duration_seconds = self._progress.elapsed_seconds, + loss_sparkline = _json.dumps(sparkline), + output_dir = output_dir, + error_message = error_message, + ) + self._run_finalized = True + except Exception: + logger.warning( + "Failed to finalize run in DB (status=%s)", status, exc_info = True + ) + + def _flush_metrics_to_db(self) -> None: + """Flush buffered metrics to the database and update live progress.""" + if ( + not self._metric_buffer + or not self.current_job_id + or not self._db_run_created + ): + return + # Cap buffer to prevent unbounded memory growth + if len(self._metric_buffer) > 500: + logger.warning( + "Metric buffer exceeded 500 entries (%d) — trimming oldest", + len(self._metric_buffer), + ) + self._metric_buffer = self._metric_buffer[-500:] + # Snapshot before insert so metrics arriving during the write are preserved + batch = list(self._metric_buffer) + try: + from storage.studio_db import insert_metrics_batch, update_run_progress + + insert_metrics_batch(self.current_job_id, batch) + del self._metric_buffer[: len(batch)] + update_run_progress( + id = self.current_job_id, + step = self._progress.step, + loss = self._progress.loss + if ( + self._progress.loss is not None + and math.isfinite(self._progress.loss) + ) + else None, + duration_seconds = self._progress.elapsed_seconds, + ) + except Exception: + # Leave buffer intact for retry on next flush + logger.warning("Failed to flush metrics to DB", exc_info = True) @staticmethod def _read_queue(q: Any, timeout_sec: float) -> Optional[dict]: @@ -561,11 +843,13 @@ class TrainingBackend: if progress.error: title = f"Error: {progress.error}" elif progress.is_completed: - title = f"Training completed! Final loss: {progress.loss:.4f}" + loss_str = f"{progress.loss:.4f}" if progress.loss is not None else "--" + title = f"Training completed! Final loss: {loss_str}" elif progress.status_message: title = progress.status_message elif progress.step > 0: - title = f"Epoch: {progress.epoch} | Step: {progress.step}/{progress.total_steps} | Loss: {progress.loss:.4f}" + loss_str = f"{progress.loss:.4f}" if progress.loss is not None else "--" + title = f"Epoch: {progress.epoch} | Step: {progress.step}/{progress.total_steps} | Loss: {loss_str}" else: title = "Training Loss" diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index ccd805b7ac..891dfca8f7 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -16,15 +16,294 @@ from __future__ import annotations import structlog from loggers import get_logger import os +import platform +import shutil import sys import time import traceback +import json +import subprocess as _sp from pathlib import Path from typing import Any +import urllib.error +import urllib.request logger = get_logger(__name__) +_CAUSAL_CONV1D_RELEASE_TAG = "v1.6.1.post4" +_CAUSAL_CONV1D_PACKAGE_VERSION = "1.6.1" +_MAMBA_SSM_RELEASE_TAG = "v2.3.1" +_MAMBA_SSM_PACKAGE_VERSION = "2.3.1" + + +def _model_wants_causal_conv1d(model_name: str) -> bool: + name = model_name.lower() + return any( + key in name + for key in ( + "qwen3.5", + "qwen3_5", + "qwen3-next", + "qwen3_next", + "nemotron_h", + "nemotron-h", + "nemotron-3-nano", + "falcon_h1", + "falcon-h1", + "granite-4.0-h", + "granitemoehybrid", + "lfm2", + ) + ) + + +def _causal_conv1d_platform_tag() -> str | None: + machine = platform.machine().lower() + if sys.platform.startswith("linux"): + if machine in {"x86_64", "amd64"}: + return "linux_x86_64" + if machine in {"aarch64", "arm64"}: + return "linux_aarch64" + return None + # No prebuilt wheels published for macOS or Windows + return None + + +def _probe_causal_conv1d_env() -> dict[str, str] | None: + try: + probe = _sp.run( + [ + sys.executable, + "-c", + ( + "import json, sys, re, torch; " + "parts = torch.__version__.split('+', 1)[0].split('.')[:2]; " + "minor = re.sub(r'[^0-9].*', '', parts[1]) if len(parts) > 1 else '0'; " + "torch_mm = parts[0] + '.' + minor; " + "print(json.dumps({" + "'python_tag': f'cp{sys.version_info.major}{sys.version_info.minor}', " + "'torch_mm': torch_mm, " + "'cuda_major': str(int(str(torch.version.cuda).split('.', 1)[0])) if torch.version.cuda else '', " + "'cxx11abi': str(torch._C._GLIBCXX_USE_CXX11_ABI).upper()" + "}))" + ), + ], + stdout = _sp.PIPE, + stderr = _sp.PIPE, + text = True, + timeout = 30, + ) + except _sp.TimeoutExpired: + logger.warning("Torch environment probe timed out after 30s") + return None + if probe.returncode != 0: + logger.warning( + "Failed to probe torch environment for causal-conv1d wheel:\n%s", + probe.stdout, + ) + return None + + try: + return json.loads(probe.stdout.strip()) + except json.JSONDecodeError: + logger.warning( + "Failed to parse torch environment probe output: %s", probe.stdout + ) + return None + + +def _direct_wheel_url( + *, + filename_prefix: str, + package_version: str, + release_tag: str, + release_base_url: str, + env: dict[str, str] | None = None, +) -> str | None: + env = env or _probe_causal_conv1d_env() + platform_tag = _causal_conv1d_platform_tag() + if env is None or platform_tag is None or not env.get("cuda_major"): + return None + + filename = ( + f"{filename_prefix}-{package_version}" + f"+cu{env['cuda_major']}torch{env['torch_mm']}" + f"cxx11abi{env['cxx11abi']}-{env['python_tag']}-{env['python_tag']}-{platform_tag}.whl" + ) + return f"{release_base_url}/{release_tag}/{filename}" + + +def _url_exists(url: str) -> bool: + try: + request = urllib.request.Request(url, method = "HEAD") + with urllib.request.urlopen(request, timeout = 10): + return True + except urllib.error.HTTPError as exc: + if exc.code == 404: + return False + logger.warning("Unexpected HTTP error while probing %s: %s", url, exc) + return False + except Exception as exc: + logger.warning("Failed to probe %s: %s", url, exc) + return False + + +def _install_package_wheel_first( + *, + event_queue: Any, + import_name: str, + display_name: str, + pypi_name: str, + pypi_version: str, + filename_prefix: str, + release_tag: str, + release_base_url: str, +) -> None: + try: + __import__(import_name) + logger.info("%s already installed", display_name) + return + except ImportError: + pass + + env = _probe_causal_conv1d_env() + wheel_url = _direct_wheel_url( + filename_prefix = filename_prefix, + package_version = pypi_version, + release_tag = release_tag, + release_base_url = release_base_url, + env = env, + ) + + if wheel_url is None: + logger.info("No compatible %s wheel candidate", display_name) + else: + if _url_exists(wheel_url): + _send_status(event_queue, f"Installing prebuilt {display_name} wheel...") + installed = False + # Try uv first if available, then fall back to pip + if shutil.which("uv"): + uv_cmd = [ + "uv", + "pip", + "install", + "--python", + sys.executable, + "--no-deps", + wheel_url, + ] + result = _sp.run( + uv_cmd, + stdout = _sp.PIPE, + stderr = _sp.STDOUT, + text = True, + ) + if result.returncode == 0: + installed = True + else: + logger.warning( + "uv failed to install %s wheel:\n%s", + display_name, + result.stdout, + ) + if not installed: + pip_cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--no-deps", + wheel_url, + ] + result = _sp.run( + pip_cmd, + stdout = _sp.PIPE, + stderr = _sp.STDOUT, + text = True, + ) + if result.returncode == 0: + installed = True + else: + logger.warning( + "pip failed to install %s wheel:\n%s", + display_name, + result.stdout, + ) + if installed: + logger.info("Installed prebuilt %s wheel successfully", display_name) + return + else: + logger.info("No published %s wheel found: %s", display_name, wheel_url) + + _send_status(event_queue, f"Installing {display_name} from PyPI...") + pypi_cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--no-build-isolation", + "--no-deps", + "--no-cache-dir", + f"{pypi_name}=={pypi_version}", + ] + result = _sp.run( + pypi_cmd, + stdout = _sp.PIPE, + stderr = _sp.STDOUT, + text = True, + ) + if result.returncode != 0: + logger.error("Failed to install %s from PyPI:\n%s", display_name, result.stdout) + return + + logger.info("Installed %s from PyPI", display_name) + + +def _ensure_causal_conv1d_fast_path(event_queue: Any, model_name: str) -> None: + if not _model_wants_causal_conv1d(model_name): + return + + _install_package_wheel_first( + event_queue = event_queue, + import_name = "causal_conv1d", + display_name = "causal-conv1d", + pypi_name = "causal-conv1d", + pypi_version = _CAUSAL_CONV1D_PACKAGE_VERSION, + filename_prefix = "causal_conv1d", + release_tag = _CAUSAL_CONV1D_RELEASE_TAG, + release_base_url = "https://github.com/Dao-AILab/causal-conv1d/releases/download", + ) + + +_SSM_MODEL_SUBSTRINGS = ( + "nemotron_h", + "nemotron-h", + "nemotron-3-nano", + "falcon_h1", + "falcon-h1", + "granite-4.0-h", + "granitemoehybrid", +) + + +def _ensure_mamba_ssm(event_queue: Any, model_name: str) -> None: + if not any(sub in model_name.lower() for sub in _SSM_MODEL_SUBSTRINGS): + return + + logger.info("SSM model detected; setting up mamba-ssm after causal-conv1d") + _install_package_wheel_first( + event_queue = event_queue, + import_name = "mamba_ssm", + display_name = "mamba-ssm", + pypi_name = "mamba-ssm", + pypi_version = _MAMBA_SSM_PACKAGE_VERSION, + filename_prefix = "mamba_ssm", + release_tag = _MAMBA_SSM_RELEASE_TAG, + release_base_url = "https://github.com/state-spaces/mamba/releases/download", + ) + + def _activate_transformers_version(model_name: str) -> None: """Activate the correct transformers version BEFORE any ML imports. @@ -121,45 +400,24 @@ def run_training_process( model_name, ) - # ── 1b. Auto-install mamba-ssm for SSM/hybrid models (NemotronH, Falcon-H1) ── - _SSM_MODEL_SUBSTRINGS = ("nemotron_h", "nemotron-3-nano", "falcon_h1", "falcon-h1") - if any(sub in model_name.lower() for sub in _SSM_MODEL_SUBSTRINGS): - try: - import mamba_ssm # noqa: F401 - - logger.info("mamba-ssm already installed") - except ImportError: - logger.info( - "SSM model detected — installing mamba-ssm and causal-conv1d (this may take several minutes)..." - ) - _send_status( - event_queue, "Installing mamba-ssm (first time only, ~7 min)..." - ) - import subprocess as _sp - - # --no-build-isolation: compile against current torch (no version conflicts) - # --no-deps: don't pull in torch/transformers/triton (already installed) - for _pkg in ["causal_conv1d", "mamba_ssm"]: - _r = _sp.run( - [ - sys.executable, - "-m", - "pip", - "install", - "--no-build-isolation", - "--no-deps", - "--no-cache-dir", - _pkg, - ], - stdout = _sp.PIPE, - stderr = _sp.STDOUT, - text = True, - ) - if _r.returncode != 0: - logger.error("Failed to install %s:\n%s", _pkg, _r.stdout) - else: - logger.info("Installed %s successfully", _pkg) - logger.info("mamba-ssm installation complete") + # ── 1b. Set up causal-conv1d first, then install mamba-ssm if needed ── + try: + _ensure_causal_conv1d_fast_path(event_queue, model_name) + _ensure_mamba_ssm(event_queue, model_name) + except Exception as exc: + event_queue.put( + { + "type": "error", + "error": ( + f"Please choose another model to train, since " + f"causal-conv1d / mamba-ssm failed to install " + f"with error: {exc}" + ), + "stack": traceback.format_exc(limit = 20), + "ts": time.time(), + } + ) + return # ── 1c. Set fork start method so dataset.map() can multiprocess ── # The parent launched us via spawn (clean process), but the compiled @@ -242,7 +500,7 @@ def run_training_process( # Wire up progress callback → event_queue def _on_progress(progress: TrainingProgress): - has_train_loss = progress.step >= 0 and progress.loss > 0 + has_train_loss = progress.step > 0 and progress.loss is not None has_eval_loss = progress.eval_loss is not None if has_train_loss or has_eval_loss: event_queue.put( @@ -918,7 +1176,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) -> def on_log(self, args, state, control, logs = None, **kwargs): if not logs: return - loss_value = logs.get("loss", logs.get("train_loss", 0.0)) + loss_value = logs.get("loss", logs.get("train_loss", None)) current_step = state.global_step elapsed = time.time() - training_start_time @@ -934,7 +1192,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) -> "step": current_step, "epoch": round(state.epoch, 2) if state.epoch else 0, "loss": loss_value, - "learning_rate": logs.get("learning_rate", 0.0), + "learning_rate": logs.get("learning_rate", None), "total_steps": total_steps, "elapsed_seconds": elapsed, "eta_seconds": eta, diff --git a/studio/backend/main.py b/studio/backend/main.py index 7134c5a783..5e647f6312 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -49,6 +49,7 @@ from routes import ( export_router, inference_router, models_router, + training_history_router, training_router, ) from auth import storage @@ -73,6 +74,17 @@ async def lifespan(app: FastAPI): # Detect hardware first — sets DEVICE global used everywhere detect_hardware() + from storage.studio_db import cleanup_orphaned_runs + + try: + cleanup_orphaned_runs() + except Exception as exc: + import structlog + + structlog.get_logger(__name__).warning( + "cleanup_orphaned_runs failed at startup: %s", exc + ) + # Pre-cache the helper GGUF model for LLM-assisted dataset detection. # Runs in a background thread so it doesn't block server startup. import threading @@ -149,6 +161,9 @@ app.include_router(inference_router, prefix = "/v1", tags = ["openai-compat"]) app.include_router(datasets_router, prefix = "/api/datasets", tags = ["datasets"]) app.include_router(data_recipe_router, prefix = "/api/data-recipe", tags = ["data-recipe"]) app.include_router(export_router, prefix = "/api/export", tags = ["export"]) +app.include_router( + training_history_router, prefix = "/api/train", tags = ["training-history"] +) # ============ Health and System Endpoints ============ diff --git a/studio/backend/models/__init__.py b/studio/backend/models/__init__.py index 11cf215f54..a4fbbbe6ee 100644 --- a/studio/backend/models/__init__.py +++ b/studio/backend/models/__init__.py @@ -10,6 +10,11 @@ from .training import ( TrainingJobResponse, TrainingStatus, TrainingProgress, + TrainingRunSummary, + TrainingRunListResponse, + TrainingRunMetrics, + TrainingRunDetailResponse, + TrainingRunDeleteResponse, ) from .models import ( CheckpointInfo, @@ -71,6 +76,11 @@ __all__ = [ "TrainingJobResponse", "TrainingStatus", "TrainingProgress", + "TrainingRunSummary", + "TrainingRunListResponse", + "TrainingRunMetrics", + "TrainingRunDetailResponse", + "TrainingRunDeleteResponse", # Model management schemas "ModelDetails", "LocalModelInfo", diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py index 342e44cc09..68791aa7a8 100644 --- a/studio/backend/models/training.py +++ b/studio/backend/models/training.py @@ -177,8 +177,8 @@ class TrainingProgress(BaseModel): job_id: str = Field(..., description = "Training job identifier") step: int = Field(..., description = "Current training step") total_steps: int = Field(..., description = "Total training steps") - loss: float = Field(..., description = "Current loss value") - learning_rate: float = Field(..., description = "Current learning rate") + loss: Optional[float] = Field(None, description = "Current loss value") + learning_rate: Optional[float] = Field(None, description = "Current learning rate") progress_percent: float = Field( ..., description = "Progress percentage (0.0 to 100.0)" ) @@ -196,3 +196,59 @@ class TrainingProgress(BaseModel): eval_loss: Optional[float] = Field( None, description = "Eval loss from the most recent evaluation step" ) + + +class TrainingRunSummary(BaseModel): + """Summary of a training run for list views.""" + + id: str + status: Literal["running", "completed", "stopped", "error"] + model_name: str + dataset_name: str + started_at: str + ended_at: Optional[str] = None + total_steps: Optional[int] = None + final_step: Optional[int] = None + final_loss: Optional[float] = None + output_dir: Optional[str] = None + duration_seconds: Optional[float] = None + error_message: Optional[str] = None + loss_sparkline: Optional[List[float]] = None + + +class TrainingRunListResponse(BaseModel): + """Response for listing training runs.""" + + runs: List[TrainingRunSummary] + total: int + + +class TrainingRunMetrics(BaseModel): + """Metrics arrays for a training run, using paired step arrays per metric.""" + + step_history: List[int] = Field(default_factory = list) + loss_history: List[float] = Field(default_factory = list) + loss_step_history: List[int] = Field(default_factory = list) + lr_history: List[float] = Field(default_factory = list) + lr_step_history: List[int] = Field(default_factory = list) + grad_norm_history: List[float] = Field(default_factory = list) + grad_norm_step_history: List[int] = Field(default_factory = list) + eval_loss_history: List[float] = Field(default_factory = list) + eval_step_history: List[int] = Field(default_factory = list) + final_epoch: Optional[float] = None + final_num_tokens: Optional[int] = None + + +class TrainingRunDetailResponse(BaseModel): + """Response for a single training run with config and metrics.""" + + run: TrainingRunSummary + config: dict + metrics: TrainingRunMetrics + + +class TrainingRunDeleteResponse(BaseModel): + """Response for deleting a training run.""" + + status: str + message: str diff --git a/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml b/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml index d4770a0b05..c9d988bd9d 100644 --- a/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml +++ b/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml @@ -11,7 +11,7 @@ version = "0.1.0" description = "Local Data Designer unstructured seed reader plugin" requires-python = ">=3.11" dependencies = [ - "data-designer-engine>=0.5.1,<0.6", + "data-designer-engine>=0.5.4,<0.6", "pandas>=2,<3", "pymupdf>=1.24.0", "pymupdf4llm>=0.0.17", diff --git a/studio/backend/requirements/single-env/data-designer-deps.txt b/studio/backend/requirements/single-env/data-designer-deps.txt index 9cd0db99e4..0cb42db01d 100644 --- a/studio/backend/requirements/single-env/data-designer-deps.txt +++ b/studio/backend/requirements/single-env/data-designer-deps.txt @@ -1,8 +1,10 @@ # Data Designer runtime deps installed explicitly (single-env mode). -# DuckDB 1.5 removed Relation.record_batch(); keep <1.5 until upstream ships the fix. +# Synced with data-designer-engine==0.5.4 requirements. anyascii<1,>=0.3.3 -duckdb<1.5,>=1.1.3 +chardet<6,>=3.0.2 +duckdb<2,>=1.5.0 faker<21,>=20.1.0 +fsspec<2026,>=2025.3.0 httpx<1,>=0.27.2 httpx-retries<1,>=0.4.2 json-repair<1,>=0.48.0 @@ -10,12 +12,13 @@ jsonpath-rust-bindings<2,>=1.0 jsonschema<5,>=4.0.0 lxml<7,>=6.0.2 marko<3,>=2.1.2 +mcp<2,>=1.26.0 networkx<4,>=3.0 -python-json-logger<4,>=3 ruff<1,>=0.14.10 scipy<2,>=1.11.0 sqlfluff<4,>=3.2.0 tiktoken<1,>=0.8.0 +# Unstructured-seed plugin deps (plugin installed with --no-deps) pymupdf>=1.24.0 pymupdf4llm>=0.0.17 mammoth>=1.8.0 diff --git a/studio/backend/requirements/single-env/data-designer.txt b/studio/backend/requirements/single-env/data-designer.txt index 8daa1eca43..2e0ba62249 100644 --- a/studio/backend/requirements/single-env/data-designer.txt +++ b/studio/backend/requirements/single-env/data-designer.txt @@ -1,5 +1,5 @@ # Install Data Designer in same env as Unsloth. -data-designer==0.5.2 -data-designer-config==0.5.2 -data-designer-engine==0.5.2 +data-designer==0.5.4 +data-designer-config==0.5.4 +data-designer-engine==0.5.4 prompt-toolkit>=3,<4 diff --git a/studio/backend/routes/__init__.py b/studio/backend/routes/__init__.py index b45eff821b..e79f6553f9 100644 --- a/studio/backend/routes/__init__.py +++ b/studio/backend/routes/__init__.py @@ -12,6 +12,7 @@ from routes.datasets import router as datasets_router from routes.auth import router as auth_router from routes.data_recipe import router as data_recipe_router from routes.export import router as export_router +from routes.training_history import router as training_history_router __all__ = [ "training_router", @@ -21,4 +22,5 @@ __all__ = [ "auth_router", "data_recipe_router", "export_router", + "training_history_router", ] diff --git a/studio/backend/routes/training.py b/studio/backend/routes/training.py index 4f8054f80e..4cfb060dee 100644 --- a/studio/backend/routes/training.py +++ b/studio/backend/routes/training.py @@ -14,6 +14,7 @@ import structlog from loggers import get_logger import asyncio from datetime import datetime +import uuid as _uuid # Add backend directory to path # The backend code should be in the same directory structure @@ -115,15 +116,11 @@ async def start_training( backend = get_training_backend() - # Generate job ID and attach to backend for later status/progress calls - job_id = f"job_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - backend.current_job_id = job_id - - # Check if training is already active + # Check if training is already active (before mutating any state) if backend.is_training_active(): existing_job_id: Optional[str] = getattr(backend, "current_job_id", "") return TrainingJobResponse( - job_id = existing_job_id or job_id, + job_id = existing_job_id or "", status = "error", message = ( "Training is already in progress. " @@ -132,6 +129,12 @@ async def start_training( error = "Training already active", ) + # Generate job ID — passed into start_training() which sets it on the + # backend only after confirming the old pump thread is dead. + job_id = ( + f"job_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{_uuid.uuid4().hex[:8]}" + ) + # Validate dataset paths if provided if request.local_datasets: request.local_datasets = _validate_local_dataset_paths( @@ -248,12 +251,12 @@ async def start_training( logger.warning("Could not shut down export subprocess: %s", e) # start_training now spawns a subprocess (non-blocking) - success = backend.start_training(**training_kwargs) + success = backend.start_training(job_id = job_id, **training_kwargs) if not success: progress_error = backend.trainer.training_progress.error return TrainingJobResponse( - job_id = job_id, + job_id = backend.current_job_id or "", status = "error", message = progress_error or "Failed to start training subprocess", error = progress_error or "subprocess_start_failed", @@ -345,7 +348,7 @@ async def reset_training( error = None, status_message = "Ready to train", step = 0, - loss = 0.0, + loss = None, epoch = 0, total_steps = 0, ) @@ -419,8 +422,8 @@ async def get_training_status( "epoch": getattr(progress, "epoch", 0), "step": getattr(progress, "step", 0), "total_steps": getattr(progress, "total_steps", 0), - "loss": getattr(progress, "loss", 0.0), - "learning_rate": getattr(progress, "learning_rate", 0.0), + "loss": getattr(progress, "loss", None), + "learning_rate": getattr(progress, "learning_rate", None), } # Build metric history for chart recovery after SSE reconnection @@ -526,8 +529,8 @@ async def stream_training_progress( # ── Helpers ────────────────────────────────────────────── def build_progress( step: int, - loss: float, - learning_rate: float, + loss: Optional[float], + learning_rate: Optional[float], total_steps: int, epoch: Optional[float] = None, progress: Optional[Any] = None, @@ -604,10 +607,10 @@ async def stream_training_progress( loss_val = ( backend.loss_history[i] if i < len(backend.loss_history) - else 0.0 + else None ) lr_val = ( - backend.lr_history[i] if i < len(backend.lr_history) else 0.0 + backend.lr_history[i] if i < len(backend.lr_history) else None ) tp_replay = getattr( getattr(backend, "trainer", None), "training_progress", None @@ -645,8 +648,8 @@ async def stream_training_progress( initial_progress = build_progress( step = 0, - loss = 0.0, - learning_rate = 0.0, + loss = None, + learning_rate = None, total_steps = initial_total_steps, epoch = initial_epoch, progress = tp, @@ -660,9 +663,9 @@ async def stream_training_progress( if backend.step_history: final_step = backend.step_history[-1] final_loss = ( - backend.loss_history[-1] if backend.loss_history else 0.0 + backend.loss_history[-1] if backend.loss_history else None ) - final_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + final_lr = backend.lr_history[-1] if backend.lr_history else None final_total_steps = ( getattr(tp, "total_steps", final_step) if tp else final_step ) @@ -680,7 +683,9 @@ async def stream_training_progress( ) else: yield format_sse( - build_progress(-1, 0.0, 0.0, 0, progress = tp).model_dump_json(), + build_progress( + -1, None, None, 0, progress = tp + ).model_dump_json(), event = "complete", event_id = 0, ) @@ -698,9 +703,9 @@ async def stream_training_progress( if backend.step_history: current_step = backend.step_history[-1] current_loss = ( - backend.loss_history[-1] if backend.loss_history else 0.0 + backend.loss_history[-1] if backend.loss_history else None ) - current_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + current_lr = backend.lr_history[-1] if backend.lr_history else None tp_inner = getattr( getattr(backend, "trainer", None), "training_progress", None ) @@ -763,8 +768,8 @@ async def stream_training_progress( ) preparing_payload = build_progress( 0, - 0.0, - 0.0, + None, + None, prep_total, progress = tp_prep, ) @@ -781,7 +786,7 @@ async def stream_training_progress( getattr(backend, "trainer", None), "training_progress", None ) timeout_payload = build_progress( - last_step, 0.0, 0.0, 0, progress = tp_timeout + last_step, None, None, 0, progress = tp_timeout ) yield format_sse( timeout_payload.model_dump_json(), @@ -797,7 +802,7 @@ async def stream_training_progress( tp_error = getattr( getattr(backend, "trainer", None), "training_progress", None ) - error_payload = build_progress(0, 0.0, 0.0, 0, progress = tp_error) + error_payload = build_progress(0, None, None, 0, progress = tp_error) yield format_sse( error_payload.model_dump_json(), event = "error", @@ -807,8 +812,8 @@ async def stream_training_progress( # ── Final "complete" event ─────────────────────────────── final_step = backend.step_history[-1] if backend.step_history else last_step - final_loss = backend.loss_history[-1] if backend.loss_history else 0.0 - final_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + final_loss = backend.loss_history[-1] if backend.loss_history else None + final_lr = backend.lr_history[-1] if backend.lr_history else None final_tp = getattr(getattr(backend, "trainer", None), "training_progress", None) final_total_steps = ( getattr(final_tp, "total_steps", final_step) if final_tp else final_step diff --git a/studio/backend/routes/training_history.py b/studio/backend/routes/training_history.py new file mode 100644 index 0000000000..597c4424c0 --- /dev/null +++ b/studio/backend/routes/training_history.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +""" +Training history API routes — browse, view, and delete past training runs. +""" + +import json + +from fastapi import APIRouter, Depends, HTTPException, Query +from loggers import get_logger + +from auth.authentication import get_current_subject +from models import ( + TrainingRunDeleteResponse, + TrainingRunDetailResponse, + TrainingRunListResponse, + TrainingRunMetrics, + TrainingRunSummary, +) +from storage.studio_db import delete_run, get_run, get_run_metrics, list_runs + +logger = get_logger(__name__) + +router = APIRouter() + + +@router.get("/runs", response_model = TrainingRunListResponse) +async def list_training_runs( + limit: int = Query(50, ge = 1, le = 200), + offset: int = Query(0, ge = 0), + current_subject: str = Depends(get_current_subject), +): + """List training runs, newest first.""" + result = list_runs(limit = limit, offset = offset) + return TrainingRunListResponse( + runs = [TrainingRunSummary(**r) for r in result["runs"]], + total = result["total"], + ) + + +@router.get("/runs/{run_id}", response_model = TrainingRunDetailResponse) +async def get_training_run_detail( + run_id: str, + current_subject: str = Depends(get_current_subject), +): + """Get a single training run with full config and metrics.""" + run = get_run(run_id) + if run is None: + raise HTTPException(status_code = 404, detail = f"Run {run_id} not found") + + try: + config = json.loads(run.get("config_json", "{}")) + except (json.JSONDecodeError, TypeError): + logger.debug("Failed to parse config_json for run %s", run_id) + config = {} + + metrics_data = get_run_metrics(run_id) + + return TrainingRunDetailResponse( + run = TrainingRunSummary(**{k: v for k, v in run.items() if k != "config_json"}), + config = config, + metrics = TrainingRunMetrics(**metrics_data), + ) + + +@router.delete("/runs/{run_id}", response_model = TrainingRunDeleteResponse) +async def delete_training_run( + run_id: str, + current_subject: str = Depends(get_current_subject), +): + """Delete a training run and its metrics (CASCADE).""" + run = get_run(run_id) + if run is None: + raise HTTPException(status_code = 404, detail = f"Run {run_id} not found") + if run["status"] == "running": + raise HTTPException( + status_code = 409, detail = "Cannot delete a running training run" + ) + logger.info("Deleting training run %s", run_id) + delete_run(run_id) + return TrainingRunDeleteResponse( + status = "deleted", + message = f"Run {run_id} deleted", + ) diff --git a/studio/backend/storage/__init__.py b/studio/backend/storage/__init__.py new file mode 100644 index 0000000000..32014236c6 --- /dev/null +++ b/studio/backend/storage/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 diff --git a/studio/backend/storage/studio_db.py b/studio/backend/storage/studio_db.py new file mode 100644 index 0000000000..4af19df42b --- /dev/null +++ b/studio/backend/storage/studio_db.py @@ -0,0 +1,362 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +""" +SQLite storage for training run history and metrics. + +Follows the same pattern as auth/storage.py — module-level functions, +raw sqlite3, per-function connections. Enhancements over auth: + - WAL mode for concurrent read/write access + - PRAGMA foreign_keys = ON for CASCADE deletes +""" + +import json +import logging +import sqlite3 +import threading + +logger = logging.getLogger(__name__) +from typing import Optional + +from utils.paths import studio_db_path, ensure_dir + +_schema_lock = threading.Lock() +_schema_ready = False + + +def _ensure_schema(conn: sqlite3.Connection) -> None: + """Create tables and indexes if they don't exist. Called once per process.""" + conn.execute("PRAGMA journal_mode=WAL") + conn.execute( + """ + CREATE TABLE IF NOT EXISTS training_runs ( + id TEXT NOT NULL PRIMARY KEY, + status TEXT NOT NULL DEFAULT 'running', + model_name TEXT NOT NULL, + dataset_name TEXT NOT NULL, + config_json TEXT NOT NULL, + started_at TEXT NOT NULL, + ended_at TEXT, + total_steps INTEGER, + final_step INTEGER, + final_loss REAL, + output_dir TEXT, + error_message TEXT, + duration_seconds REAL, + loss_sparkline TEXT + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS training_metrics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id TEXT NOT NULL REFERENCES training_runs(id) ON DELETE CASCADE, + step INTEGER NOT NULL, + loss REAL, + learning_rate REAL, + grad_norm REAL, + eval_loss REAL, + epoch REAL, + num_tokens INTEGER, + elapsed_seconds REAL, + UNIQUE(run_id, step) + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_metrics_run_id ON training_metrics(run_id)" + ) + + +def get_connection() -> sqlite3.Connection: + """Open studio.db with WAL mode, create tables once per process, enable foreign keys.""" + global _schema_ready + db_path = studio_db_path() + ensure_dir(db_path.parent) + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + # foreign_keys is session-scoped, must be set per connection + conn.execute("PRAGMA foreign_keys=ON") + if not _schema_ready: + with _schema_lock: + if not _schema_ready: + try: + _ensure_schema(conn) + _schema_ready = True + except Exception: + conn.close() + raise + return conn + + +def create_run( + id: str, + model_name: str, + dataset_name: str, + config_json: str, + started_at: str, + total_steps: Optional[int], +) -> None: + conn = get_connection() + try: + conn.execute( + """ + INSERT INTO training_runs (id, model_name, dataset_name, config_json, started_at, total_steps) + VALUES (?, ?, ?, ?, ?, ?) + """, + (id, model_name, dataset_name, config_json, started_at, total_steps), + ) + conn.commit() + finally: + conn.close() + + +def update_run_total_steps(id: str, total_steps: int) -> None: + conn = get_connection() + try: + conn.execute( + "UPDATE training_runs SET total_steps = ? WHERE id = ?", + (total_steps, id), + ) + conn.commit() + finally: + conn.close() + + +def update_run_progress( + id: str, step: int, loss: Optional[float], duration_seconds: Optional[float] +) -> None: + """Update current progress on a running training run (called on each metric flush).""" + conn = get_connection() + try: + conn.execute( + "UPDATE training_runs SET final_step = ?, final_loss = ?, duration_seconds = ? WHERE id = ?", + (step, loss, duration_seconds, id), + ) + conn.commit() + finally: + conn.close() + + +def finish_run( + id: str, + status: str, + ended_at: str, + final_step: Optional[int], + final_loss: Optional[float], + duration_seconds: Optional[float], + loss_sparkline: Optional[str] = None, + output_dir: Optional[str] = None, + error_message: Optional[str] = None, +) -> None: + conn = get_connection() + try: + conn.execute( + """ + UPDATE training_runs + SET status = ?, ended_at = ?, final_step = ?, final_loss = ?, + duration_seconds = ?, loss_sparkline = ?, output_dir = ?, + error_message = ? + WHERE id = ? + """, + ( + status, + ended_at, + final_step, + final_loss, + duration_seconds, + loss_sparkline, + output_dir, + error_message, + id, + ), + ) + conn.commit() + finally: + conn.close() + + +def insert_metrics_batch(run_id: str, metrics: list[dict]) -> None: + if not metrics: + return + conn = get_connection() + try: + conn.executemany( + """ + INSERT INTO training_metrics + (run_id, step, loss, learning_rate, grad_norm, eval_loss, epoch, num_tokens, elapsed_seconds) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(run_id, step) DO UPDATE SET + loss = COALESCE(excluded.loss, loss), + learning_rate = COALESCE(excluded.learning_rate, learning_rate), + grad_norm = COALESCE(excluded.grad_norm, grad_norm), + eval_loss = COALESCE(excluded.eval_loss, eval_loss), + epoch = COALESCE(excluded.epoch, epoch), + num_tokens = COALESCE(excluded.num_tokens, num_tokens), + elapsed_seconds = COALESCE(excluded.elapsed_seconds, elapsed_seconds) + """, + [ + ( + run_id, + m.get("step"), + m.get("loss"), + m.get("learning_rate"), + m.get("grad_norm"), + m.get("eval_loss"), + m.get("epoch"), + m.get("num_tokens"), + m.get("elapsed_seconds"), + ) + for m in metrics + ], + ) + conn.commit() + finally: + conn.close() + + +def list_runs(limit: int = 50, offset: int = 0) -> dict: + conn = get_connection() + try: + total = conn.execute("SELECT COUNT(*) FROM training_runs").fetchone()[0] + rows = conn.execute( + """ + SELECT id, status, model_name, dataset_name, started_at, ended_at, + total_steps, final_step, final_loss, output_dir, + duration_seconds, error_message, loss_sparkline + FROM training_runs + ORDER BY started_at DESC + LIMIT ? OFFSET ? + """, + (limit, offset), + ).fetchall() + runs = [] + for row in rows: + run = dict(row) + sparkline = run.get("loss_sparkline") + if sparkline: + try: + run["loss_sparkline"] = json.loads(sparkline) + except (json.JSONDecodeError, TypeError): + logger.debug( + "Failed to parse loss_sparkline for run %s", run.get("id") + ) + run["loss_sparkline"] = None + runs.append(run) + return {"runs": runs, "total": total} + finally: + conn.close() + + +def get_run(id: str) -> Optional[dict]: + conn = get_connection() + try: + row = conn.execute("SELECT * FROM training_runs WHERE id = ?", (id,)).fetchone() + if row is None: + return None + run = dict(row) + sparkline = run.get("loss_sparkline") + if sparkline: + try: + run["loss_sparkline"] = json.loads(sparkline) + except (json.JSONDecodeError, TypeError): + logger.debug("Failed to parse loss_sparkline for run %s", id) + run["loss_sparkline"] = None + return run + finally: + conn.close() + + +def get_run_metrics(id: str) -> dict: + """Return metric arrays for a run, using paired step arrays per metric.""" + conn = get_connection() + try: + rows = conn.execute( + """ + SELECT step, loss, learning_rate, grad_norm, eval_loss, epoch, + num_tokens, elapsed_seconds + FROM training_metrics + WHERE run_id = ? + ORDER BY step + """, + (id,), + ).fetchall() + + step_history: list[int] = [] + loss_history: list[float] = [] + loss_step_history: list[int] = [] + lr_history: list[float] = [] + lr_step_history: list[int] = [] + grad_norm_history: list[float] = [] + grad_norm_step_history: list[int] = [] + eval_loss_history: list[float] = [] + eval_step_history: list[int] = [] + final_epoch: float | None = None + final_num_tokens: int | None = None + + for row in rows: + step = row["step"] + step_history.append(step) + if step > 0 and row["loss"] is not None: + loss_history.append(row["loss"]) + loss_step_history.append(step) + if step > 0 and row["learning_rate"] is not None: + lr_history.append(row["learning_rate"]) + lr_step_history.append(step) + if step > 0 and row["grad_norm"] is not None: + grad_norm_history.append(row["grad_norm"]) + grad_norm_step_history.append(step) + if step > 0 and row["eval_loss"] is not None: + eval_loss_history.append(row["eval_loss"]) + eval_step_history.append(step) + if row["epoch"] is not None: + final_epoch = row["epoch"] + if row["num_tokens"] is not None: + final_num_tokens = row["num_tokens"] + + return { + "step_history": step_history, + "loss_history": loss_history, + "loss_step_history": loss_step_history, + "lr_history": lr_history, + "lr_step_history": lr_step_history, + "grad_norm_history": grad_norm_history, + "grad_norm_step_history": grad_norm_step_history, + "eval_loss_history": eval_loss_history, + "eval_step_history": eval_step_history, + "final_epoch": final_epoch, + "final_num_tokens": final_num_tokens, + } + finally: + conn.close() + + +def delete_run(id: str) -> None: + conn = get_connection() + try: + conn.execute("DELETE FROM training_runs WHERE id = ?", (id,)) + conn.commit() + finally: + conn.close() + + +def cleanup_orphaned_runs() -> None: + """Mark any 'running' rows as errored on startup (server restarted mid-training).""" + from datetime import datetime, timezone + + conn = get_connection() + try: + conn.execute( + """ + UPDATE training_runs + SET status = 'error', + error_message = 'Server restarted during training', + ended_at = ? + WHERE status = 'running' + """, + (datetime.now(timezone.utc).isoformat(),), + ) + conn.commit() + finally: + conn.close() diff --git a/studio/backend/utils/downsample.py b/studio/backend/utils/downsample.py new file mode 100644 index 0000000000..bccf6a23b7 --- /dev/null +++ b/studio/backend/utils/downsample.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Generic numeric downsampling utility.""" + + +def downsample(values: list[float], target_count: int) -> list[float]: + """Reduce a list to target_count points via evenly-spaced index sampling.""" + if len(values) <= target_count: + return list(values) + if target_count <= 0: + return [] + if target_count == 1: + return [values[-1]] + indices = [ + round(i * (len(values) - 1) / (target_count - 1)) for i in range(target_count) + ] + return [values[i] for i in indices] diff --git a/studio/backend/utils/paths/__init__.py b/studio/backend/utils/paths/__init__.py index 90df216f96..789052f372 100644 --- a/studio/backend/utils/paths/__init__.py +++ b/studio/backend/utils/paths/__init__.py @@ -16,6 +16,7 @@ from .storage_roots import ( exports_root, auth_root, auth_db_path, + studio_db_path, tmp_root, seed_uploads_root, unstructured_seed_cache_root, @@ -45,6 +46,7 @@ __all__ = [ "exports_root", "auth_root", "auth_db_path", + "studio_db_path", "tmp_root", "seed_uploads_root", "unstructured_seed_cache_root", diff --git a/studio/backend/utils/paths/storage_roots.py b/studio/backend/utils/paths/storage_roots.py index f14887e119..626e868275 100644 --- a/studio/backend/utils/paths/storage_roots.py +++ b/studio/backend/utils/paths/storage_roots.py @@ -49,6 +49,10 @@ def auth_db_path() -> Path: return auth_root() / "auth.db" +def studio_db_path() -> Path: + return studio_root() / "studio.db" + + def tmp_root() -> Path: return Path(tempfile.gettempdir()) / "unsloth-studio" diff --git a/studio/frontend/.gitignore b/studio/frontend/.gitignore index bf7ac45ef1..f43950477e 100644 --- a/studio/frontend/.gitignore +++ b/studio/frontend/.gitignore @@ -11,6 +11,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +bun.lock dist dist-ssr test/ diff --git a/studio/frontend/bun.lock b/studio/frontend/bun.lock deleted file mode 100644 index 5504aea3d3..0000000000 --- a/studio/frontend/bun.lock +++ /dev/null @@ -1,2483 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "unsloth-theme", - "dependencies": { - "@assistant-ui/react": "^0.12.19", - "@assistant-ui/react-markdown": "^0.12.3", - "@assistant-ui/react-streamdown": "^0.1.2", - "@base-ui/react": "^1.2.0", - "@dagrejs/dagre": "^2.0.4", - "@dagrejs/graphlib": "^3.0.4", - "@fontsource-variable/figtree": "^5.2.10", - "@fontsource-variable/inter": "^5.2.8", - "@fontsource-variable/space-grotesk": "^5.2.10", - "@hugeicons/core-free-icons": "^3.1.1", - "@hugeicons/react": "^1.1.5", - "@huggingface/hub": "^2.9.0", - "@langchain/core": "^1.1.27", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@streamdown/cjk": "1.0.2", - "@streamdown/code": "1.0.2", - "@streamdown/math": "1.0.2", - "@streamdown/mermaid": "1.0.2", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-router": "^1.159.10", - "@tanstack/react-table": "^8.21.3", - "@toolwind/corner-shape": "^0.0.8-3", - "@types/canvas-confetti": "^1.9.0", - "@xyflow/react": "^12.10.0", - "assistant-stream": "^0.3.2", - "canvas-confetti": "^1.9.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "date-fns": "^4.1.0", - "dexie": "^4.3.0", - "framer-motion": "^11.18.2", - "js-yaml": "^4.1.1", - "katex": "^0.16.28", - "lucide-react": "^0.577.0", - "mammoth": "^1.11.0", - "motion": "^12.34.0", - "next": "^16.1.6", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.3", - "react": "^19.2.4", - "react-day-picker": "^9.13.2", - "react-dom": "^19.2.4", - "react-resizable-panels": "^4.6.4", - "recharts": "3.7.0", - "remark-gfm": "^4.0.1", - "shadcn": "^3.8.4", - "sonner": "^2.0.7", - "streamdown": "2.3.0", - "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.18", - "tw-animate-css": "^1.4.0", - "tw-shimmer": "^0.4.6", - "unpdf": "^1.4.0", - "zustand": "^5.0.11", - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@eslint/js": "^9.39.1", - "@types/js-yaml": "^4.0.9", - "@types/node": "^24.10.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.26", - "globals": "^16.5.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.55.0", - "vite": "^7.3.1", - }, - }, - }, - "packages": { - "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - - "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], - - "@assistant-ui/core": ["@assistant-ui/core@0.1.7", "", { "dependencies": { "assistant-stream": "^0.3.6", "nanoid": "^5.1.6" }, "peerDependencies": { "@assistant-ui/store": "^0.2.3", "@assistant-ui/tap": "^0.5.3", "@types/react": "*", "assistant-cloud": "^0.1.22", "react": "^18 || ^19", "zustand": "^5.0.11" }, "optionalPeers": ["@types/react", "assistant-cloud", "react", "zustand"] }, "sha512-219T42ihVOicbJXZLWgD2CW5Bylg9Nk7geC331X4RfJxTDYlm2zIjViGlGaqfj6URXBp6kMulO2BTUrHGmAvdw=="], - - "@assistant-ui/react": ["@assistant-ui/react@0.12.19", "", { "dependencies": { "@assistant-ui/core": "^0.1.7", "@assistant-ui/store": "^0.2.3", "@assistant-ui/tap": "^0.5.3", "@radix-ui/primitive": "^1.1.3", "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-context": "^1.1.3", "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "@radix-ui/react-use-escape-keydown": "^1.1.1", "assistant-cloud": "^0.1.22", "assistant-stream": "^0.3.6", "nanoid": "^5.1.6", "radix-ui": "^1.4.3", "react-textarea-autosize": "^8.5.9", "zod": "^4.3.6", "zustand": "^5.0.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-scAf0o8cwjuHT9Y44EFGXcE2y6BSmpeMvt0NxOn8+Y/HBlNttQMLNvrM0p2AjacXCUufagiafAnWybzBV3nKEQ=="], - - "@assistant-ui/react-markdown": ["@assistant-ui/react-markdown@0.12.4", "", { "dependencies": { "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "classnames": "^2.5.1", "react-markdown": "^10.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.11", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-6TD9guiuLJxJoOwSjNHUYAVma2ctDCG9uypUqKHE0OUhDwTDD3NsMvTnQ0n0Lh8nnCEwVglOwKKlSEYpV7SnWA=="], - - "@assistant-ui/react-streamdown": ["@assistant-ui/react-streamdown@0.1.3", "", { "dependencies": { "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "streamdown": "^2.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.11", "@streamdown/cjk": "^1.0.0", "@streamdown/code": "^1.0.0", "@streamdown/math": "^1.0.0", "@streamdown/mermaid": "^1.0.0", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@streamdown/cjk", "@streamdown/code", "@streamdown/math", "@streamdown/mermaid", "@types/react"] }, "sha512-n1UCjXQ3svmDtJBMJj/vXqz/BqAQBuy7myrXeymz2tD9l+ENQgqu2JY5ir3J19juJTe5lsi/P3+tOJ2C1jc/nw=="], - - "@assistant-ui/store": ["@assistant-ui/store@0.2.3", "", { "dependencies": { "use-effect-event": "^2.0.3" }, "peerDependencies": { "@assistant-ui/tap": "^0.5.3", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-daStbgSQiX7+csqK6Cvo7A8p8UZkTCSMxBHxbhJvwrlVbp7BRJWTxq3U3rpTkSGIar23SXIyVRRfXU8VW7pswA=="], - - "@assistant-ui/tap": ["@assistant-ui/tap@0.5.3", "", { "peerDependencies": { "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-wy06ksqF2LfFxe4JXy31Ns89N/be1Dy3c+mG363cFHFp3CbLkRu8CrCN2SQSgCkXt628E+D8QyzqdBcl9kD4NQ=="], - - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - - "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], - - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], - - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], - - "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], - - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], - - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - - "@base-ui/react": ["@base-ui/react@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.5", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw=="], - - "@base-ui/utils": ["@base-ui/utils@0.2.5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - - "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], - - "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], - - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.1", "", { "dependencies": { "@chevrotain/gast": "11.1.1", "@chevrotain/types": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw=="], - - "@chevrotain/gast": ["@chevrotain/gast@11.1.1", "", { "dependencies": { "@chevrotain/types": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg=="], - - "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.1", "", {}, "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg=="], - - "@chevrotain/types": ["@chevrotain/types@11.1.1", "", {}, "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw=="], - - "@chevrotain/utils": ["@chevrotain/utils@11.1.1", "", {}, "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ=="], - - "@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="], - - "@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="], - - "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], - - "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.52.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w=="], - - "@ecies/ciphers": ["@ecies/ciphers@0.2.5", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "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.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], - - "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - - "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], - - "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], - - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], - - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - - "@fontsource-variable/figtree": ["@fontsource-variable/figtree@5.2.10", "", {}, "sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA=="], - - "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], - - "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.2.10", "", {}, "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w=="], - - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], - - "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@3.1.1", "", {}, "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg=="], - - "@hugeicons/react": ["@hugeicons/react@1.1.5", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-JX/iDz3oO7hWdVqbjwFwRrAjHk8h2vI+mBkNzp4JcXG3t4idoupfjon73nLOA7cr27m0M8hrRC1Q2h6nEBGKVA=="], - - "@huggingface/hub": ["@huggingface/hub@2.10.3", "", { "dependencies": { "@huggingface/tasks": "^0.19.85" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-qSk4FcVFdTGx0lNpFyy7p2KwgAPCsjM2+tupG/MGToEvUGVLsy+dCmela1BcU/VvJNweCtnH5HwdNr7IQa4Zzw=="], - - "@huggingface/tasks": ["@huggingface/tasks@0.19.86", "", {}, "sha512-eab/6J9m+0Z8xw3X2EPPioMLIjFNYjox9nONTmzzgWj0vq6+iMWsMt4tlwrZKLlxxJbFp+acn20VXZi3ejLlng=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - - "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], - - "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], - - "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], - - "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], - - "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], - - "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@langchain/core": ["@langchain/core@1.1.28", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "uuid": "^10.0.0", "zod": "^3.25.76 || ^4" } }, "sha512-6FAGdezEp8zHY92LtnsAiv54KaG41nBdsuukk+R+1484edV20cVOyIc36ANuGKPx0pmYFCBWhCUdO0jxB/zn2Q=="], - - "@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], - - "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], - - "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], - - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], - - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], - - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], - - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], - - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], - - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], - - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], - - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], - - "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], - - "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - - "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], - - "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], - - "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], - - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - - "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], - - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - - "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="], - - "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], - - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], - - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], - - "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="], - - "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], - - "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], - - "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], - - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], - - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], - - "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="], - - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], - - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - - "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], - - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], - - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], - - "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="], - - "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], - - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], - - "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], - - "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="], - - "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], - - "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="], - - "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="], - - "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], - - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], - - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - - "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], - - "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="], - - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], - - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], - - "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], - - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], - - "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - - "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], - - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], - - "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="], - - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], - - "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], - - "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="], - - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], - - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - - "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], - - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - - "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], - - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], - - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], - - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], - - "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], - - "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - - "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], - - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], - - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="], - - "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="], - - "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="], - - "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="], - - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - - "@streamdown/cjk": ["@streamdown/cjk@1.0.2", "", { "dependencies": { "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-5OOuZjj2Lnae92Zmg2gA5hloSbcKj25gv+QY4iKbYI+iRsiGWbgmYxmgxNUSO9SR6BKOCy783UHN1HM/QEUpdw=="], - - "@streamdown/code": ["@streamdown/code@1.0.2", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-QKLS3sC8no5y0YvhGLA+ZjtNhznWU09IvFcjRKgSA35ulckMLw3b5T1ha+o1DaW8BS8l0zceLPFZa3/X9+agWQ=="], - - "@streamdown/math": ["@streamdown/math@1.0.2", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g=="], - - "@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="], - - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], - - "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], - - "@tanstack/react-router": ["@tanstack/react-router@1.162.9", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.162.9", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-APbwKAF+YgSNpHAaA+FdgrmfI/7+qa9hApuVO9+P0IVksJayNIWFQ/6AFG90WQiTYWk64RI1R9cFV2K9Z+j2pQ=="], - - "@tanstack/react-store": ["@tanstack/react-store@0.9.1", "", { "dependencies": { "@tanstack/store": "0.9.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA=="], - - "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], - - "@tanstack/router-core": ["@tanstack/router-core@1.162.9", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eG7C0oVtZbFOkfvsaF8UyGuNjEc1BfIfD5EzQNwG4vqLKOAyY5SMFBCNjabAi2sglRhL0ZOwKon1SExusU5fxA=="], - - "@tanstack/store": ["@tanstack/store@0.9.1", "", {}, "sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg=="], - - "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - - "@toolwind/corner-shape": ["@toolwind/corner-shape@0.0.8-3", "", { "dependencies": { "@types/node": "^20.4.1" } }, "sha512-MPIF81F2bhtXbzEeXF0vnL+PKpnopCHOzBspOkK8osMzWQvPUujZn2XZOMdsu4DF6wsVbbRYQtdsJr486HmIPQ=="], - - "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="], - - "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - - "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], - - "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], - - "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], - - "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], - - "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], - - "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - - "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], - - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - - "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], - - "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], - - "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], - - "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - - "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], - - "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - - "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], - - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - - "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], - - "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], - - "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], - - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - - "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], - - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - - "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - - "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], - - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - - "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], - - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], - - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - - "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], - - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], - - "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], - - "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], - - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], - - "@xyflow/react": ["@xyflow/react@12.10.1", "", { "dependencies": { "@xyflow/system": "0.0.75", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q=="], - - "@xyflow/system": ["@xyflow/system@0.0.75", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@6.14.0", "", { "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" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - - "assistant-cloud": ["assistant-cloud@0.1.22", "", { "dependencies": { "assistant-stream": "^0.3.6" } }, "sha512-AEE9shV+oFrGDv/MRTRERctNKpIYS0n34UpAQXXICiOkSWD6QZnS1ljLqruFko7fJoT5CIWq8dNeJWdzQLTBLg=="], - - "assistant-stream": ["assistant-stream@0.3.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-Ne/uTseMIiZx740dTbr/SWxONM8nYj4Z5BRmUfqQN+TNgtOCgWOlC/oTUQ+A7LIUHtmGbcoyZwDf8yd2RASnDA=="], - - "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], - - "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], - - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="], - - "canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="], - - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - - "chevrotain": ["chevrotain@11.1.1", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", "@chevrotain/regexp-to-ast": "11.1.1", "@chevrotain/types": "11.1.1", "@chevrotain/utils": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ=="], - - "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], - - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - - "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], - - "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], - - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], - - "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - - "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "console-table-printer": ["console-table-printer@2.15.0", "", { "dependencies": { "simple-wcswidth": "^1.1.2" } }, "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw=="], - - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - - "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - - "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], - - "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="], - - "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], - - "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], - - "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], - - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - - "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], - - "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], - - "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], - - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - - "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], - - "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], - - "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], - - "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], - - "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], - - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - - "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], - - "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], - - "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - - "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - - "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], - - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - - "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], - - "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], - - "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], - - "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], - - "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - - "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], - - "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], - - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - - "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], - - "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - - "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="], - - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - - "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], - - "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="], - - "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - - "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], - - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - - "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], - - "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - - "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - - "dexie": ["dexie@4.3.0", "", {}, "sha512-5EeoQpJvMKHe6zWt/FSIIuRa3CWlZeIl6zKXt+Lz7BU6RoRRLgX9dZEynRfXrkLcldKYCBiz7xekTEylnie1Ug=="], - - "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - - "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], - - "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], - - "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], - - "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "eciesjs": ["eciesjs@0.4.17", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], - - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], - - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "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.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.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" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], - - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], - - "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - - "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], - - "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - - "get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "graphql": ["graphql@16.13.0", "", {}, "sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA=="], - - "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], - - "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], - - "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], - - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - - "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], - - "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], - - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], - - "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - - "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - - "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], - - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - - "hono": ["hono@4.12.2", "", {}, "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg=="], - - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], - - "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - - "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - - "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - - "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], - - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - - "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], - - "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], - - "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], - - "katex": ["katex@0.16.33", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - - "langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="], - - "langsmith": ["langsmith@0.5.6", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^5.6.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-T/RA2l2MsTYX0z1aW8rQ2hBQZEOuXV2v/6tkfG6R5EotJTKMpw1dERCbvP8ezOP8otyWfnNlQA88ZnMRsQ7CHA=="], - - "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], - - "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], - - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - - "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "mammoth": ["mammoth@1.11.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ=="], - - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - - "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - - "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], - - "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], - - "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], - - "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], - - "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - - "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], - - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], - - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], - - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "mermaid": ["mermaid@11.12.3", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ=="], - - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], - - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - - "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="], - - "micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.3.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gSPnxgHDDqXYOBvQRq6lerrq9mjDhdtKn+7XETuXjxWcL62yZEfUdA28Ml1I2vDIPfAOIKLa0h2XDSGkInGHFQ=="], - - "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="], - - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], - - "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], - - "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], - - "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], - - "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], - - "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], - - "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - - "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], - - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], - - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], - - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], - - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], - - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], - - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], - - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], - - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], - - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], - - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], - - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], - - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], - - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - - "minimatch": ["minimatch@3.1.3", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], - - "motion": ["motion@12.34.3", "", { "dependencies": { "framer-motion": "^12.34.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw=="], - - "motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], - - "motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="], - - "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], - - "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], - - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], - - "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - - "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], - - "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], - - "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], - - "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="], - - "optionator": ["optionator@0.9.4", "", { "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" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], - - "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], - - "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], - - "p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], - - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], - - "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - - "parse-json": ["parse-json@5.2.0", "", { "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" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], - - "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - - "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], - - "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], - - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - - "react-day-picker": ["react-day-picker@9.13.2", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg=="], - - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - - "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], - - "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - - "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], - - "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - - "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], - - "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - - "react-resizable-panels": ["react-resizable-panels@4.6.5", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-pmQP6qv9KmsesNMvWVNvVfVJAwYSOWWbAOAtrPR8Cre20+j1NWIlyft0btjtDQE+OepXmI6g3VPrCXQY0oD7+Q=="], - - "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - - "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="], - - "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], - - "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="], - - "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], - - "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - - "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], - - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - - "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], - - "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], - - "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="], - - "remark-cjk-friendly-gfm-strikethrough": ["remark-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly-gfm-strikethrough": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-bXfMZtsaomK6ysNN/UGRIcasQAYkC10NtPmP0oOHOV8YOhA2TXmwRXCku4qOzjIFxAPfish5+XS0eIug2PzNZA=="], - - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - - "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], - - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - - "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - - "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - - "remend": ["remend@1.2.1", "", {}, "sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - - "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], - - "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], - - "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], - - "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="], - - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], - - "side-channel": ["side-channel@1.1.0", "", { "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" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-wcswidth": ["simple-wcswidth@1.1.2", "", {}, "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - - "streamdown": ["streamdown@2.3.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.1", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw=="], - - "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - - "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], - - "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], - - "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - - "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], - - "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - - "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], - - "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], - - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - - "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], - - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="], - - "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], - - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - - "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], - - "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], - - "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], - - "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - - "tw-shimmer": ["tw-shimmer@0.4.6", "", { "peerDependencies": { "tailwindcss": ">=4.0.0-0" } }, "sha512-Wg3Qy9bcIHw6v2hqFzsvBiuIVHey2HyjDPYY/ozkDCWDYNPirxs1GoIs8FCrNtc0YTb+/wuSySAB7DjbTY6uGw=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], - - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - - "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], - - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], - - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - - "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], - - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "unpdf": ["unpdf@1.4.0", "", { "peerDependencies": { "@napi-rs/canvas": "^0.1.69" }, "optionalPeers": ["@napi-rs/canvas"] }, "sha512-TahIk0xdH/4jh/MxfclzU79g40OyxtP00VnEUZdEkJoYtXAHWLiir6t3FC6z3vDqQTzc2ZHcla6uEiVTNjejuA=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], - - "use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="], - - "use-effect-event": ["use-effect-event@2.0.3", "", { "peerDependencies": { "react": "^18.3 || ^19.0.0-0" } }, "sha512-fz1en+z3fYXCXx3nMB8hXDMuygBltifNKZq29zDx+xNJ+1vEs6oJlYd9sK31vxJ0YI534VUsHEBY0k2BATsmBQ=="], - - "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="], - - "use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="], - - "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - - "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - - "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], - - "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], - - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], - - "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], - - "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - - "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], - - "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yargs": ["yargs@17.7.2", "", { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - - "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - - "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="], - - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - - "@assistant-ui/core/assistant-stream": ["assistant-stream@0.3.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-NdtSRrQfWCDA/aqQ1xhobf/xnhuMZkhFAw9xzAt5iAoL3ouxVXOowSRN87OL4MYBQEvqtcjw9/CE6YcsXoBtuw=="], - - "@assistant-ui/react/assistant-stream": ["assistant-stream@0.3.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-NdtSRrQfWCDA/aqQ1xhobf/xnhuMZkhFAw9xzAt5iAoL3ouxVXOowSRN87OL4MYBQEvqtcjw9/CE6YcsXoBtuw=="], - - "@assistant-ui/react/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - - "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "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" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - - "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - - "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-alert-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-context-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-form/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], - - "@radix-ui/react-form/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-hover-card/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-menubar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-menubar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-navigation-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-one-time-password-field/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-one-time-password-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-password-toggle-field/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-password-toggle-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-radio-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-radio-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-slider/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-slider/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-switch/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-toast/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-toast/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-toggle-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-toggle-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-toolbar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-toolbar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], - - "@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@toolwind/corner-shape/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], - - "@ts-morph/common/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "@xyflow/react/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - - "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - - "assistant-cloud/assistant-stream": ["assistant-stream@0.3.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-NdtSRrQfWCDA/aqQ1xhobf/xnhuMZkhFAw9xzAt5iAoL3ouxVXOowSRN87OL4MYBQEvqtcjw9/CE6YcsXoBtuw=="], - - "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - - "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - - "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], - - "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "langsmith/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "langsmith/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], - - "mammoth/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - - "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], - - "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "motion/framer-motion": ["framer-motion@12.34.3", "", { "dependencies": { "motion-dom": "^12.34.3", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q=="], - - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - - "radix-ui/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], - - "radix-ui/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], - - "radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "shadcn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - - "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - - "@dotenvx/dotenvx/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - - "@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], - - "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - - "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-avatar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-form/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-hover-card/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-menubar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-one-time-password-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-password-toggle-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-radio-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-slider/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-toast/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-toolbar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@toolwind/corner-shape/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], - - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "cmdk/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], - - "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], - - "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - - "motion/framer-motion/motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], - - "motion/framer-motion/motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], - - "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - } -} diff --git a/studio/frontend/package.json b/studio/frontend/package.json index 4b40759d62..d9bab4de7e 100644 --- a/studio/frontend/package.json +++ b/studio/frontend/package.json @@ -3,6 +3,9 @@ "private": true, "version": "0.0.0", "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, "scripts": { "dev": "vite", "build": "tsc -b && vite build", @@ -35,7 +38,7 @@ "@streamdown/code": "1.0.2", "@streamdown/math": "1.0.2", "@streamdown/mermaid": "1.0.2", - "@tailwindcss/vite": "^4.1.18", + "@tailwindcss/vite": "^4.2.2", "@tanstack/react-router": "^1.159.10", "@tanstack/react-table": "^8.21.3", "@toolwind/corner-shape": "^0.0.8-3", @@ -48,7 +51,6 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "dexie": "^4.3.0", - "framer-motion": "^11.18.2", "js-yaml": "^4.1.1", "katex": "^0.16.28", "lucide-react": "^0.577.0", @@ -80,13 +82,13 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^6.0.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.26", "globals": "^16.5.0", "typescript": "~5.9.3", "typescript-eslint": "^8.55.0", - "vite": "^7.3.1" + "vite": "^8.0.1" } } diff --git a/studio/frontend/public/unsloth.ico b/studio/frontend/public/unsloth.ico new file mode 100644 index 0000000000..974a6ed059 Binary files /dev/null and b/studio/frontend/public/unsloth.ico differ diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index 0bbbe94fdc..5e84b9175e 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -4,19 +4,39 @@ "use client"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; +import { preprocessLaTeX } from "@/lib/latex"; import { INTERNAL, useMessagePartText } from "@assistant-ui/react"; import { Copy02Icon, Tick02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { code } from "@streamdown/code"; -import { math } from "@streamdown/math"; +import { createMathPlugin } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { DownloadIcon, Maximize2Icon, Minimize2Icon } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Block, type BlockProps, Streamdown } from "streamdown"; import "katex/dist/katex.min.css"; import { AudioPlayer } from "./audio-player"; +const math = createMathPlugin({ singleDollarTextMath: true }); const { withSmoothContextProvider } = INTERNAL; + +const STREAMDOWN_COMPONENTS = { + a: ({ + href, + children, + ...props + }: React.ComponentProps<"a">) => ( + + {children} + + ), +}; const COPY_RESET_MS = 2000; const MERMAID_SOURCE_RE = /```mermaid\s*([\s\S]*?)```/i; const CODE_FENCE_RE = /^```([^\r\n`]*)\r?\n([\s\S]*?)\r?\n?```$/; @@ -375,6 +395,7 @@ const AUDIO_PLAYER_RE = //; const MarkdownTextImpl = () => { const { text, status } = useMessagePartText(); + const processedText = useMemo(() => preprocessLaTeX(text), [text]); const audioMatch = text.match(AUDIO_PLAYER_RE); if (audioMatch) { @@ -387,6 +408,7 @@ const MarkdownTextImpl = () => { mode="streaming" isAnimating={status.type === "running"} plugins={{ code, math, mermaid }} + components={STREAMDOWN_COMPONENTS} controls={{ code: false, mermaid: { @@ -399,7 +421,7 @@ const MarkdownTextImpl = () => { shikiTheme={["github-light", "github-dark"]} BlockComponent={StreamdownBlock} > - {text} + {processedText} ); diff --git a/studio/frontend/src/components/assistant-ui/reasoning.tsx b/studio/frontend/src/components/assistant-ui/reasoning.tsx index 0e37f6d433..6b2c7a05e7 100644 --- a/studio/frontend/src/components/assistant-ui/reasoning.tsx +++ b/studio/frontend/src/components/assistant-ui/reasoning.tsx @@ -17,7 +17,6 @@ import { type ReasoningGroupComponent, type ReasoningMessagePartComponent, useAuiState, - useScrollLock, } from "@assistant-ui/react"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { Idea01Icon } from "@hugeicons/core-free-icons"; @@ -34,6 +33,7 @@ import { useState, } from "react"; const ANIMATION_DURATION = 200; +const AUTO_SCROLL_THRESHOLD_PX = 24; export const reasoningVariants = cva("aui-reasoning-root mb-4 w-full", { variants: { @@ -68,8 +68,49 @@ function ReasoningRoot({ ...props }: ReasoningRootProps) { const collapsibleRef = useRef(null); + const lockCleanupRef = useRef<(() => void) | null>(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); - const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); + + useEffect(() => { + return () => { + lockCleanupRef.current?.(); + }; + }, []); + + const lockScroll = useCallback(() => { + lockCleanupRef.current?.(); + + const animatedElement = collapsibleRef.current; + if (!animatedElement) return; + + let scrollContainer: HTMLElement | null = animatedElement; + while (scrollContainer) { + const { overflowY } = getComputedStyle(scrollContainer); + if (overflowY === "scroll" || overflowY === "auto") { + break; + } + scrollContainer = scrollContainer.parentElement; + } + if (!scrollContainer) return; + + const scrollPosition = scrollContainer.scrollTop; + const resetPosition = () => { + scrollContainer.scrollTop = scrollPosition; + }; + + scrollContainer.addEventListener("scroll", resetPosition); + let timeoutId: ReturnType | null = null; + const cleanup = () => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + timeoutId = null; + } + scrollContainer.removeEventListener("scroll", resetPosition); + lockCleanupRef.current = null; + }; + timeoutId = setTimeout(cleanup, ANIMATION_DURATION); + lockCleanupRef.current = cleanup; + }, []); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : uncontrolledOpen; @@ -220,6 +261,8 @@ function ReasoningText({ }: ComponentProps<"div"> & { streaming?: boolean }) { const scrollRef = useRef(null); const shouldAutoScrollRef = useRef(true); + const detachedFromBottomRef = useRef(false); + const lastScrollTopRef = useRef(0); useEffect(() => { if (!(streaming && scrollRef.current)) { @@ -227,8 +270,25 @@ function ReasoningText({ } const el = scrollRef.current; const updateAutoScroll = () => { + const currentScrollTop = el.scrollTop; + if (currentScrollTop < lastScrollTopRef.current) { + detachedFromBottomRef.current = true; + } const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; - shouldAutoScrollRef.current = distanceFromBottom <= 24; + if ( + detachedFromBottomRef.current && + distanceFromBottom <= AUTO_SCROLL_THRESHOLD_PX + ) { + detachedFromBottomRef.current = false; + } + shouldAutoScrollRef.current = !detachedFromBottomRef.current; + lastScrollTopRef.current = currentScrollTop; + }; + const handleWheel = (event: WheelEvent) => { + if (event.deltaY < 0) { + detachedFromBottomRef.current = true; + shouldAutoScrollRef.current = false; + } }; const observer = new MutationObserver(() => { if (shouldAutoScrollRef.current) { @@ -236,16 +296,19 @@ function ReasoningText({ } }); el.addEventListener("scroll", updateAutoScroll); + el.addEventListener("wheel", handleWheel, { passive: true }); observer.observe(el, { childList: true, subtree: true, characterData: true, }); - shouldAutoScrollRef.current = true; - el.scrollTop = el.scrollHeight; + lastScrollTopRef.current = el.scrollTop; + detachedFromBottomRef.current = false; + updateAutoScroll(); return () => { observer.disconnect(); el.removeEventListener("scroll", updateAutoScroll); + el.removeEventListener("wheel", handleWheel); }; }, [streaming]); diff --git a/studio/frontend/src/components/assistant-ui/sources.tsx b/studio/frontend/src/components/assistant-ui/sources.tsx index 18ee03cad0..da97ff66b5 100644 --- a/studio/frontend/src/components/assistant-ui/sources.tsx +++ b/studio/frontend/src/components/assistant-ui/sources.tsx @@ -43,7 +43,8 @@ function SourceIcon({ }: ComponentProps<"span"> & { url: string; size?: number }) { const [hasError, setHasError] = useState(false); const domain = extractDomain(url); - const sizeClass = `size-${size}`; + const SIZE_CLASSES: Record = { 3: "size-3", 4: "size-4", 5: "size-5" }; + const sizeClass = SIZE_CLASSES[size] ?? "size-3"; if (hasError) { return ( diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 91cba5b02b..d688822815 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -16,6 +16,7 @@ import { WebSearchToolUI } from "@/components/assistant-ui/tool-ui-web-search"; import { PythonToolUI } from "@/components/assistant-ui/tool-ui-python"; import { TerminalToolUI } from "@/components/assistant-ui/tool-ui-terminal"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; +import { AnimatedShinyText } from "@/components/ui/animated-shiny-text"; import { Button } from "@/components/ui/button"; import { sentAudioNames } from "@/features/chat/api/chat-adapter"; import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils"; @@ -35,7 +36,7 @@ import { useAuiEvent, useAuiState, } from "@assistant-ui/react"; -import { motion } from "framer-motion"; +import { motion } from "motion/react"; import { ArrowDownIcon, ArrowUpIcon, @@ -88,9 +89,8 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({ }} /> - + - !thread.isEmpty}> {!hideComposer && } @@ -541,6 +541,17 @@ const MessageError: FC = () => { ); }; +const GeneratingIndicator: FC = () => { + const show = useAuiState( + ({ message }) => + message.content.length === 0 && message.status?.type === "running", + ); + if (!show) return null; + return ( + Generating... + ); +}; + const AssistantMessage: FC = () => { return ( { data-role="assistant" >
+ -
{children}
+
{children}
); } @@ -226,7 +226,7 @@ function ToolFallbackArgs({ return (
@@ -251,7 +251,7 @@ function ToolFallbackResult({
     

@@ -316,7 +316,7 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({ return ( diff --git a/studio/frontend/src/components/assistant-ui/tool-group.tsx b/studio/frontend/src/components/assistant-ui/tool-group.tsx index f29adb510f..bf7a6a9a25 100644 --- a/studio/frontend/src/components/assistant-ui/tool-group.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-group.tsx @@ -26,11 +26,11 @@ const toolGroupVariants = cva("aui-tool-group-root group/tool-group w-full", { variants: { variant: { outline: "corner-squircle rounded-lg border py-3", - ghost: "", + ghost: "rounded-lg bg-muted/10 py-2", muted: "corner-squircle rounded-lg border border-muted-foreground/30 bg-muted/30 py-3", }, }, - defaultVariants: { variant: "outline" }, + defaultVariants: { variant: "ghost" }, }); export type ToolGroupRootProps = Omit< @@ -76,7 +76,7 @@ function ToolGroupRoot({ {label} @@ -189,6 +188,7 @@ function ToolGroupContent({ "mt-2 flex flex-col gap-2", "group-data-[variant=outline]/tool-group-root:mt-3 group-data-[variant=outline]/tool-group-root:border-t group-data-[variant=outline]/tool-group-root:px-4 group-data-[variant=outline]/tool-group-root:pt-3", "group-data-[variant=muted]/tool-group-root:mt-3 group-data-[variant=muted]/tool-group-root:border-t group-data-[variant=muted]/tool-group-root:px-4 group-data-[variant=muted]/tool-group-root:pt-3", + "group-data-[variant=ghost]/tool-group-root:mt-1 group-data-[variant=ghost]/tool-group-root:gap-1", )} > {children} diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx index 28468a10ad..a510ed0d9e 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx @@ -7,7 +7,7 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { code as codePlugin } from "@streamdown/code"; import { CheckIcon, CodeIcon, CopyIcon, LoaderIcon } from "lucide-react"; -import { memo, useCallback, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Streamdown } from "streamdown"; import { ToolFallbackContent, @@ -28,6 +28,15 @@ function truncate(text: string): string { function CopyBtn({ text }: { text: string }) { const [copied, setCopied] = useState(false); const timer = useRef | null>(null); + + useEffect(() => { + return () => { + if (timer.current) { + clearTimeout(timer.current); + } + }; + }, []); + const copy = useCallback(() => { if (copyToClipboard(text)) { setCopied(true); @@ -98,14 +107,14 @@ const PythonToolUIImpl: ToolCallMessagePartComponent = ({ icon={CodeIcon} /> -

+
{/* Code + copy */} {code && (
)} - + {code && } {/* Output */} {isRunning ? ( diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx index 1b65b3c081..f233f951d3 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx @@ -6,7 +6,7 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { CheckIcon, CopyIcon, LoaderIcon, TerminalIcon } from "lucide-react"; -import { memo, useCallback, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import { ToolFallbackContent, ToolFallbackRoot, @@ -25,6 +25,15 @@ function truncate(text: string): string { function CopyBtn({ text }: { text: string }) { const [copied, setCopied] = useState(false); const timer = useRef | null>(null); + + useEffect(() => { + return () => { + if (timer.current) { + clearTimeout(timer.current); + } + }; + }, []); + const copy = useCallback(() => { if (copyToClipboard(text)) { setCopied(true); @@ -74,7 +83,7 @@ const TerminalToolUIImpl: ToolCallMessagePartComponent = ({ icon={TerminalIcon} /> -
+
{isRunning ? (
diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx index 0635a83ec4..d3a86846de 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx @@ -81,29 +81,27 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({ /> {isRunning ? ( -
+
Searching for “{query}”…
) : sources.length > 0 ? ( -
- {sources.map((source) => ( +
+ {sources.map((source, i) => ( - - - {source.title} - + + {source.title} ))}
) : result ? ( -
+
               {typeof result === "string"
                 ? result
diff --git a/studio/frontend/src/components/ui/collapsible.tsx b/studio/frontend/src/components/ui/collapsible.tsx
index 3566eb9859..df5347c1a7 100644
--- a/studio/frontend/src/components/ui/collapsible.tsx
+++ b/studio/frontend/src/components/ui/collapsible.tsx
@@ -2,13 +2,18 @@
 // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
 
 import { cn } from "@/lib/utils";
+import * as React from "react";
 import { Collapsible as CollapsiblePrimitive } from "radix-ui";
 
-function Collapsible({
-  ...props
-}: React.ComponentProps) {
-  return ;
-}
+const Collapsible = React.forwardRef<
+  React.ElementRef,
+  React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+  return (
+    
+  );
+});
+Collapsible.displayName = CollapsiblePrimitive.Root.displayName;
 
 function CollapsibleTrigger({
   ...props
diff --git a/studio/frontend/src/features/chat/chat-settings-sheet.tsx b/studio/frontend/src/features/chat/chat-settings-sheet.tsx
index 45c9d17888..081e3efc26 100644
--- a/studio/frontend/src/features/chat/chat-settings-sheet.tsx
+++ b/studio/frontend/src/features/chat/chat-settings-sheet.tsx
@@ -28,6 +28,8 @@ import {
   PencilEdit01Icon,
   Settings02Icon,
   SlidersHorizontalIcon,
+  UserSettings01Icon,
+  Wrench01Icon,
 } from "@hugeicons/core-free-icons";
 import { HugeiconsIcon } from "@hugeicons/react";
 import { AnimatePresence, motion } from "motion/react";
@@ -164,6 +166,39 @@ function ParamSlider({
   );
 }
 
+const COLLAPSIBLE_STATE_KEY = "unsloth_chat_collapsible_state";
+
+function loadCollapsibleState(): Record {
+  if (!canUseStorage()) return {};
+  try {
+    const raw = localStorage.getItem(COLLAPSIBLE_STATE_KEY);
+    if (!raw) return {};
+    const parsed = JSON.parse(raw);
+    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
+      return {};
+    }
+    return Object.fromEntries(
+      Object.entries(parsed).filter(
+        (entry): entry is [string, boolean] => typeof entry[1] === "boolean",
+      ),
+    );
+  } catch (error) {
+    console.warn("Failed to load collapsible state from localStorage:", error);
+    return {};
+  }
+}
+
+function saveCollapsibleOpen(label: string, open: boolean) {
+  if (!canUseStorage()) return;
+  try {
+    const state = loadCollapsibleState();
+    state[label] = open;
+    localStorage.setItem(COLLAPSIBLE_STATE_KEY, JSON.stringify(state));
+  } catch {
+    // ignore
+  }
+}
+
 function CollapsibleSection({
   icon,
   label,
@@ -175,13 +210,20 @@ function CollapsibleSection({
   children?: ReactNode;
   defaultOpen?: boolean;
 }) {
-  const [open, setOpen] = useState(defaultOpen);
+  const [open, setOpen] = useState(() => {
+    const saved = loadCollapsibleState();
+    return Object.hasOwn(saved, label) ? saved[label] : defaultOpen;
+  });
 
   return (
     
+ +
+ {isGguf && ( + <> +
+
+
Context Length
+
+ Reported by the loaded GGUF model. +
+
+ +
+
+
+
KV Cache Dtype
+
+ Quantize KV cache to reduce VRAM. Reload to apply. +
+
+ +
+ + )} + {!isGguf && ( +
+
+
Trust remote code
+
+ Allow models with custom code (e.g. Nemotron). Only enable for repos you trust. +
+
+ +
+ )} +
+
+ - + +
+ + + +
+
+ +
@@ -519,49 +632,6 @@ export function ChatSettingsPanel({ onCheckedChange={onAutoTitleChange} />
-
-
-
Trust remote code
-
- Allow models with custom code (e.g. Nemotron). Only enable for repos you trust. -
-
- -
- {isGguf && ( -
-
-
KV Cache Dtype
-
- Quantize KV cache to reduce VRAM. Reload to apply. -
-
- -
- )} - - -
diff --git a/studio/frontend/src/features/studio/historical-training-view.tsx b/studio/frontend/src/features/studio/historical-training-view.tsx new file mode 100644 index 0000000000..d3ac434bd8 --- /dev/null +++ b/studio/frontend/src/features/studio/historical-training-view.tsx @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import type { TrainingViewData } from "@/features/training"; +import { getTrainingRun } from "@/features/training"; +import type { TrainingRunDetailResponse } from "@/features/training"; +import { type ReactElement, useEffect, useState } from "react"; +import { ChartsSection } from "./sections/charts-section"; +import { ProgressSection } from "./sections/progress-section"; + +interface HistoricalTrainingViewProps { + runId: string; +} + +function normalizeTrainingMethod(config: Record): string { + const type = config?.training_type as string | undefined; + if (!type || type === "Full Finetuning") return "full"; + if (type === "LoRA/QLoRA") { + return config?.load_in_4bit ? "qlora" : "lora"; + } + return "full"; +} + +function mapToViewData(detail: TrainingRunDetailResponse): TrainingViewData { + const { run, metrics } = detail; + + const lossHistory = metrics.loss_step_history + .map((step, i) => ({ step, value: metrics.loss_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const lrHistory = metrics.lr_step_history + .map((step, i) => ({ step, value: metrics.lr_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const gradNormHistory = metrics.grad_norm_step_history + .map((step, i) => ({ step, value: metrics.grad_norm_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const evalLossHistory = metrics.eval_step_history + .map((step, i) => ({ step, value: metrics.eval_loss_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const phase = + run.status === "completed" + ? "completed" + : run.status === "stopped" + ? "stopped" + : run.status === "error" + ? "error" + : run.status === "running" + ? "training" + : "idle"; + + return { + phase, + currentStep: run.final_step ?? 0, + totalSteps: run.total_steps ?? 0, + currentLoss: run.final_loss, + currentLearningRate: metrics.lr_history.at(-1) ?? null, + currentGradNorm: metrics.grad_norm_history.at(-1) ?? null, + currentEpoch: metrics.final_epoch, + currentNumTokens: metrics.final_num_tokens ?? null, + progressPercent: + run.total_steps && run.final_step + ? (run.final_step / run.total_steps) * 100 + : 0, + elapsedSeconds: run.duration_seconds, + etaSeconds: null, + evalEnabled: evalLossHistory.length > 0, + message: + run.status === "completed" + ? "Training completed" + : run.status === "stopped" + ? "Training stopped" + : run.status === "running" + ? "Training in progress" + : run.error_message ?? "Training errored", + error: run.status === "error" ? run.error_message : null, + isTrainingRunning: false, + modelName: run.model_name, + trainingMethod: normalizeTrainingMethod(detail.config), + lossHistory, + lrHistory, + gradNormHistory, + evalLossHistory, + }; +} + +export function HistoricalTrainingView({ + runId, +}: HistoricalTrainingViewProps): ReactElement { + const [detail, setDetail] = useState(null); + const [error, setError] = useState(null); + + // Derive loading from detail/error -- no separate state needed + const loading = detail === null && error === null; + + useEffect(() => { + const controller = new AbortController(); + getTrainingRun(runId, controller.signal) + .then((result) => { + setDetail(result); + }) + .catch((err) => { + if (err instanceof DOMException && err.name === "AbortError") return; + setError(err instanceof Error ? err.message : "Failed to load run"); + }); + return () => { + controller.abort(); + // Reset on runId change so loading derives correctly for the next fetch + setDetail(null); + setError(null); + }; + }, [runId]); + + if (loading) { + return ( +
+ Loading training run... +
+ ); + } + + if (error || !detail) { + return ( +
+ {error ?? "Run not found"} +
+ ); + } + + const viewData = mapToViewData(detail); + const configOverride = detail.config + ? { + epochs: detail.config.num_epochs as number | undefined, + batchSize: detail.config.batch_size as number | undefined, + learningRate: detail.config.learning_rate as string | undefined, + maxSteps: detail.config.max_steps as number | undefined, + contextLength: detail.config.max_seq_length as number | undefined, + warmupSteps: detail.config.warmup_steps as number | undefined, + optimizerType: detail.config.optim as string | undefined, + loraRank: detail.config.lora_r as number | undefined, + loraAlpha: detail.config.lora_alpha as number | undefined, + loraDropout: detail.config.lora_dropout as number | undefined, + loraVariant: detail.config.use_rslora ? "rsLoRA" : undefined, + } + : undefined; + + return ( +
+ + +
+ ); +} diff --git a/studio/frontend/src/features/studio/history-card-grid.tsx b/studio/frontend/src/features/studio/history-card-grid.tsx new file mode 100644 index 0000000000..78859d2f81 --- /dev/null +++ b/studio/frontend/src/features/studio/history-card-grid.tsx @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import type { TrainingRunSummary } from "@/features/training"; +import { deleteTrainingRun, listTrainingRuns } from "@/features/training"; +import { cn } from "@/lib/utils"; +import { Delete02Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { type ReactElement, useCallback, useEffect, useRef, useState } from "react"; +import { Spinner } from "@/components/ui/spinner"; + +const PAGE_SIZE = 12; +const RUNNING_POLL_INTERVAL_MS = 5000; + +const statusBadge: Record< + string, + { label: string; className: string } +> = { + completed: { + label: "Completed", + className: + "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400", + }, + stopped: { + label: "Stopped", + className: + "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-400", + }, + error: { + label: "Error", + className: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-400", + }, + running: { + label: "Running", + className: + "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-400", + }, +}; + +function catmullRomPath(points: { x: number; y: number }[]): string { + if (points.length < 2) return ""; + const d = [`M${points[0]!.x.toFixed(1)},${points[0]!.y.toFixed(1)}`]; + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[Math.max(i - 1, 0)]!; + const p1 = points[i]!; + const p2 = points[i + 1]!; + const p3 = points[Math.min(i + 2, points.length - 1)]!; + const cp1x = p1.x + (p2.x - p0.x) / 6; + const cp1y = p1.y + (p2.y - p0.y) / 6; + const cp2x = p2.x - (p3.x - p1.x) / 6; + const cp2y = p2.y - (p3.y - p1.y) / 6; + d.push( + `C${cp1x.toFixed(1)},${cp1y.toFixed(1)} ${cp2x.toFixed(1)},${cp2y.toFixed(1)} ${p2.x.toFixed(1)},${p2.y.toFixed(1)}`, + ); + } + return d.join(" "); +} + +function Sparkline({ values, id }: { values: number[]; id: string }): ReactElement | null { + if (!values || values.length < 2) return null; + let min = values[0]!; + let max = values[0]!; + for (let i = 1; i < values.length; i++) { + if (values[i]! < min) min = values[i]!; + if (values[i]! > max) max = values[i]!; + } + const range = max - min || 1; + const pad = 1.5; // half stroke-width so peaks aren't clipped + const h = 32; + const w = 120; + const gradientId = `sparkFill-${id}`; + + // Build points with vertical padding so the stroke isn't clipped + const pts = values.map((v, i) => ({ + x: (i / (values.length - 1)) * w, + y: pad + (1 - (v - min) / range) * (h - pad * 2), + })); + + const linePath = catmullRomPath(pts); + const last = pts[pts.length - 1]!; + const first = pts[0]!; + const fillPath = `${linePath} L${last.x.toFixed(1)},${h} L${first.x.toFixed(1)},${h} Z`; + + return ( + + + + + + + + + + + ); +} + +function formatRelativeTime(isoDate: string): string { + const diff = Date.now() - new Date(isoDate).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + const days = Math.floor(hrs / 24); + return `${days}d ago`; +} + +function formatDuration(seconds: number | null): string { + if (seconds == null) return "--"; + const total = Math.floor(seconds); + if (total < 60) return `${total}s`; + const min = Math.floor(total / 60); + const sec = total % 60; + if (min < 60) return `${min}m ${sec}s`; + const hrs = Math.floor(min / 60); + return `${hrs}h ${min % 60}m`; +} + +interface HistoryCardGridProps { + onSelectRun: (runId: string) => void; +} + +export function HistoryCardGrid({ + onSelectRun, +}: HistoryCardGridProps): ReactElement { + const [runs, setRuns] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [deleteError, setDeleteError] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [manualFetchInFlight, setManualFetchInFlight] = useState(false); + + const userControllerRef = useRef(null); + const pollControllerRef = useRef(null); + const fetchIdRef = useRef(0); + const pollIdRef = useRef(0); + + const fetchRuns = useCallback(async (offset = 0, append = false, limit = PAGE_SIZE) => { + // Cancel any in-flight poll so its stale response can't clobber this fresher fetch + pollControllerRef.current?.abort(); + userControllerRef.current?.abort(); + const controller = new AbortController(); + userControllerRef.current = controller; + const id = ++fetchIdRef.current; + + setManualFetchInFlight(true); + setLoading(true); + setError(null); + try { + const result = await listTrainingRuns(limit, offset, controller.signal); + if (fetchIdRef.current !== id) return; + setRuns((prev) => (append ? [...prev, ...result.runs] : result.runs)); + setTotal(result.total); + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") return; + if (fetchIdRef.current !== id) return; + if (!append) setError("Failed to load training runs"); + } finally { + if (fetchIdRef.current === id) { + setLoading(false); + setManualFetchInFlight(false); + } + } + }, []); + + useEffect(() => { + void fetchRuns(0); + return () => { + userControllerRef.current?.abort(); + }; + }, [fetchRuns]); + + // Poll while any run is still "running" so the card shows live progress + const hasRunningRun = runs.some((r) => r.status === "running"); + const visibleCount = runs.length; + useEffect(() => { + if (!hasRunningRun) return; + const timer = setInterval(async () => { + if (manualFetchInFlight) return; + pollControllerRef.current?.abort(); + const controller = new AbortController(); + pollControllerRef.current = controller; + const pid = ++pollIdRef.current; + try { + const limit = Math.max(PAGE_SIZE, visibleCount); + const result = await listTrainingRuns(limit, 0, controller.signal); + if (pollIdRef.current !== pid) return; // stale poll — discard + setRuns(result.runs); + setTotal(result.total); + } catch { + // silently handle — poll will retry + } + }, RUNNING_POLL_INTERVAL_MS); + return () => { + clearInterval(timer); + pollControllerRef.current?.abort(); + }; + }, [hasRunningRun, visibleCount, manualFetchInFlight]); + + const handleDelete = async () => { + if (!deleteTarget) return; + setDeleteError(null); + try { + await deleteTrainingRun(deleteTarget); + // Optimistically remove the card so it disappears immediately + setRuns((prev) => prev.filter((r) => r.id !== deleteTarget)); + setTotal((prev) => Math.max(0, prev - 1)); + // Re-fetch preserving visible count so offsets stay consistent for "Load more" + const currentCount = runs.length - 1; + const limit = Math.max(PAGE_SIZE, currentCount); + fetchRuns(0, false, limit).catch(() => { + // Refresh failed — card is already removed, no stale display + }); + } catch { + setDeleteError("Failed to delete training run. Please try again."); + } + setDeleteTarget(null); + }; + + if (!loading && error && runs.length === 0) { + return ( +
+

{error}

+ +
+ ); + } + + if (!loading && runs.length === 0) { + return ( +
+

+ No training runs yet. Start your first training run in the Configure + tab. +

+
+ ); + } + + return ( + <> + {deleteError && ( +
+ {deleteError} +
+ )} +
+ {runs.map((run) => { + const badge = statusBadge[run.status] ?? statusBadge.error; + const isRunning = run.status === "running"; + return ( +
onSelectRun(run.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelectRun(run.id); + } + }} + > +
+ + {isRunning && } + {badge.label} + + + {formatRelativeTime(run.started_at)} + +
+
+

+ {run.model_name} +

+

+ {run.dataset_name} +

+
+ {run.loss_sparkline && run.loss_sparkline.length >= 2 && ( + + )} +
+ + Loss:{" "} + {run.final_loss != null ? run.final_loss.toFixed(4) : "--"} + + + Steps: {run.final_step ?? 0}/{run.total_steps ?? "--"} + + {formatDuration(run.duration_seconds)} +
+ {!isRunning && ( + + )} +
+ ); + })} +
+ {runs.length < total && ( +
+ +
+ )} + {loading && runs.length === 0 && ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ )} + { + if (!open) setDeleteTarget(null); + }} + > + + + Delete training run? + + This will permanently delete this training run and all its metrics. + This action cannot be undone. + + + + Cancel + void handleDelete()} + > + Delete + + + + + + ); +} diff --git a/studio/frontend/src/features/studio/live-training-view.tsx b/studio/frontend/src/features/studio/live-training-view.tsx new file mode 100644 index 0000000000..9f930ce77b --- /dev/null +++ b/studio/frontend/src/features/studio/live-training-view.tsx @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { cn } from "@/lib/utils"; +import { + useTrainingConfigStore, + useTrainingRuntimeStore, +} from "@/features/training"; +import type { TrainingViewData } from "@/features/training"; +import type { ReactElement } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { ChartsSection } from "./sections/charts-section"; +import { ProgressSection } from "./sections/progress-section"; +import { TrainingStartOverlay } from "./training-start-overlay"; + +export function LiveTrainingView(): ReactElement { + const runtime = useTrainingRuntimeStore( + useShallow((state) => ({ + jobId: state.jobId, + phase: state.phase, + message: state.message, + error: state.error, + currentStep: state.currentStep, + totalSteps: state.totalSteps, + currentEpoch: state.currentEpoch, + currentLoss: state.currentLoss, + currentLearningRate: state.currentLearningRate, + currentGradNorm: state.currentGradNorm, + currentNumTokens: state.currentNumTokens, + progressPercent: state.progressPercent, + elapsedSeconds: state.elapsedSeconds, + etaSeconds: state.etaSeconds, + evalEnabled: state.evalEnabled, + isTrainingRunning: state.isTrainingRunning, + lossHistory: state.lossHistory, + lrHistory: state.lrHistory, + gradNormHistory: state.gradNormHistory, + evalLossHistory: state.evalLossHistory, + firstStepReceived: state.firstStepReceived, + isStarting: state.isStarting, + })), + ); + + const config = useTrainingConfigStore( + useShallow((state) => ({ + selectedModel: state.selectedModel, + trainingMethod: state.trainingMethod, + })), + ); + + const viewData: TrainingViewData = { + phase: runtime.phase, + currentStep: runtime.currentStep, + totalSteps: runtime.totalSteps, + currentLoss: runtime.currentLoss, + currentLearningRate: runtime.currentLearningRate, + currentGradNorm: runtime.currentGradNorm, + currentEpoch: runtime.currentEpoch, + currentNumTokens: runtime.currentNumTokens, + progressPercent: runtime.progressPercent, + elapsedSeconds: runtime.elapsedSeconds, + etaSeconds: runtime.etaSeconds, + evalEnabled: runtime.evalEnabled, + message: runtime.message, + error: runtime.error, + isTrainingRunning: runtime.isTrainingRunning, + modelName: config.selectedModel ?? "", + trainingMethod: config.trainingMethod ?? "", + lossHistory: runtime.lossHistory, + lrHistory: runtime.lrHistory, + gradNormHistory: runtime.gradNormHistory, + evalLossHistory: runtime.evalLossHistory, + }; + + const isPreparingPhase = + runtime.phase === "downloading_model" || + runtime.phase === "downloading_dataset" || + runtime.phase === "loading_model" || + runtime.phase === "loading_dataset" || + runtime.phase === "configuring"; + const isWaitingForFirstStep = + runtime.phase === "training" && !runtime.firstStepReceived; + const showOverlay = + runtime.isStarting || + isPreparingPhase || + (isWaitingForFirstStep && runtime.currentStep <= 0); + + return ( +
+
+
+ +
+ +
+ {showOverlay ? ( + + ) : null} +
+ ); +} diff --git a/studio/frontend/src/features/studio/sections/charts-section.tsx b/studio/frontend/src/features/studio/sections/charts-section.tsx index a7dacf7e5b..4c7bf46d9d 100644 --- a/studio/frontend/src/features/studio/sections/charts-section.tsx +++ b/studio/frontend/src/features/studio/sections/charts-section.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -import { useTrainingRuntimeStore } from "@/features/training"; +import type { TrainingSeriesPoint } from "@/features/training"; import { type ReactElement, Suspense, lazy, useMemo } from "react"; const ChartsContent = lazy(() => @@ -16,42 +16,49 @@ const SKELETON_KEYS = [ "chart-skeleton-4", ]; -export function ChartsSection(): ReactElement | null { - const currentStep = useTrainingRuntimeStore((state) => state.currentStep); - const totalSteps = useTrainingRuntimeStore((state) => state.totalSteps); - const isTraining = useTrainingRuntimeStore((state) => state.isTrainingRunning); - const evalEnabled = useTrainingRuntimeStore((state) => state.evalEnabled); - const lossHistoryRaw = useTrainingRuntimeStore((state) => state.lossHistory); - const lrHistoryRaw = useTrainingRuntimeStore((state) => state.lrHistory); - const gradNormHistoryRaw = useTrainingRuntimeStore( - (state) => state.gradNormHistory, - ); - const evalLossHistoryRaw = useTrainingRuntimeStore( - (state) => state.evalLossHistory, - ); +interface ChartsSectionProps { + currentStep: number; + totalSteps: number; + isTraining: boolean; + evalEnabled: boolean; + lossHistory: TrainingSeriesPoint[]; + lrHistory: TrainingSeriesPoint[]; + gradNormHistory: TrainingSeriesPoint[]; + evalLossHistory: TrainingSeriesPoint[]; +} +export function ChartsSection({ + currentStep, + totalSteps, + isTraining, + evalEnabled, + lossHistory, + lrHistory, + gradNormHistory, + evalLossHistory, +}: ChartsSectionProps): ReactElement | null { const series = useMemo( () => ({ currentStep, totalSteps, - lossHistory: lossHistoryRaw.map((point) => ({ + lossHistory: lossHistory.map((point) => ({ step: point.step, loss: point.value, })), - lrHistory: lrHistoryRaw.map((point) => ({ + lrHistory: lrHistory.map((point) => ({ step: point.step, lr: point.value, })), - gradNormHistory: gradNormHistoryRaw.map((point) => ({ + gradNormHistory: gradNormHistory.map((point) => ({ step: point.step, gradNorm: point.value, })), - evalLossHistory: evalLossHistoryRaw.map((point) => ({ + evalLossHistory: evalLossHistory.map((point) => ({ step: point.step, loss: point.value, })), }), - [currentStep, evalLossHistoryRaw, gradNormHistoryRaw, lossHistoryRaw, lrHistoryRaw, totalSteps], + [currentStep, evalLossHistory, gradNormHistory, lossHistory, lrHistory, totalSteps], ); if ( diff --git a/studio/frontend/src/features/studio/sections/progress-section.tsx b/studio/frontend/src/features/studio/sections/progress-section.tsx index da89ce66ee..8255283183 100644 --- a/studio/frontend/src/features/studio/sections/progress-section.tsx +++ b/studio/frontend/src/features/studio/sections/progress-section.tsx @@ -26,6 +26,7 @@ import { useTrainingConfigStore, useTrainingRuntimeStore, } from "@/features/training"; +import type { TrainingViewData } from "@/features/training"; import { useGpuUtilization } from "@/hooks"; import { cn } from "@/lib/utils"; import { @@ -39,7 +40,7 @@ import { } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Link, useNavigate } from "@tanstack/react-router"; -import { type ReactElement, type ReactNode, useEffect, useState } from "react"; +import { type ReactElement, type ReactNode, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { ChartSettingsSheet } from "./charts/chart-settings-sheet"; import { @@ -61,34 +62,33 @@ function configRow( return [label, value]; } -export function ProgressSection(): ReactElement { +interface ProgressSectionProps { + data: TrainingViewData; + isHistorical?: boolean; + configOverride?: { + epochs?: number; + batchSize?: number; + learningRate?: string; + maxSteps?: number; + contextLength?: number; + warmupSteps?: number; + optimizerType?: string; + loraRank?: number; + loraAlpha?: number; + loraDropout?: number; + loraVariant?: string; + }; +} + +export function ProgressSection({ + data, + isHistorical = false, + configOverride, +}: ProgressSectionProps): ReactElement { const navigate = useNavigate(); - const runtime = useTrainingRuntimeStore( - useShallow((state) => ({ - phase: state.phase, - message: state.message, - error: state.error, - currentStep: state.currentStep, - totalSteps: state.totalSteps, - currentEpoch: state.currentEpoch, - currentLoss: state.currentLoss, - currentLearningRate: state.currentLearningRate, - currentGradNorm: state.currentGradNorm, - progressPercent: state.progressPercent, - elapsedSeconds: state.elapsedSeconds, - etaSeconds: state.etaSeconds, - currentNumTokens: state.currentNumTokens, - isTrainingRunning: state.isTrainingRunning, - lossHistory: state.lossHistory, - lrHistory: state.lrHistory, - gradNormHistory: state.gradNormHistory, - })), - ); const config = useTrainingConfigStore( useShallow((state) => ({ - selectedModel: state.selectedModel, - trainingMethod: state.trainingMethod, epochs: state.epochs, batchSize: state.batchSize, learningRate: state.learningRate, @@ -103,98 +103,92 @@ export function ProgressSection(): ReactElement { })), ); - const { stopTrainingRun } = useTrainingActions(); - const gpu = useGpuUtilization(runtime.isTrainingRunning); const [stopDialogOpen, setStopDialogOpen] = useState(false); - const [stopRequested, setStopRequested] = useState(false); + const [stopRequestedLocal, setStopRequestedLocal] = useState(false); - useEffect(() => { - if (!runtime.isTrainingRunning) { - setStopRequested(false); - } - }, [runtime.isTrainingRunning]); + // Auto-reset when training stops -- no useEffect needed + const stopRequested = data.isTrainingRunning && stopRequestedLocal; const pct = - runtime.totalSteps > 0 + data.totalSteps > 0 ? Math.min( 100, Math.max( 0, - Math.round((runtime.currentStep / runtime.totalSteps) * 100), + Math.round((data.currentStep / data.totalSteps) * 100), ), ) - : Math.round(runtime.progressPercent); + : Math.round(data.progressPercent); - const elapsed = runtime.elapsedSeconds; + const elapsed = data.elapsedSeconds; const derivedEta = elapsed != null && pct > 0 ? Math.round((elapsed * (100 - pct)) / Math.max(pct, 1)) : null; - const eta = runtime.etaSeconds ?? derivedEta; + const eta = data.etaSeconds ?? derivedEta; const stepsPerSecond = - elapsed != null && elapsed > 0 ? runtime.currentStep / elapsed : null; + elapsed != null && elapsed > 0 ? data.currentStep / elapsed : null; const showHalfwayHint = - runtime.phase === "training" && pct >= 50 && pct < 100; - const showCompletedHint = runtime.phase === "completed"; + data.phase === "training" && pct >= 50 && pct < 100; + const showCompletedHint = data.phase === "completed"; const handleCompareInChat = async () => { - setTrainingCompareHandoff(config.selectedModel); + setTrainingCompareHandoff(data.modelName); await navigate({ to: "/chat" }); }; - const requestStop = async (saveCheckpoint: boolean) => { - setStopRequested(true); - setStopDialogOpen(false); - useTrainingRuntimeStore.getState().setStopRequested(true); - try { - const ok = await stopTrainingRun(saveCheckpoint); - if (!ok) { - setStopRequested(false); - } - } catch { - setStopRequested(false); - } - }; const stoppedLoss = getDisplayMetric( - runtime.isTrainingRunning, - runtime.currentLoss, - runtime.lossHistory, + data.isTrainingRunning, + data.currentLoss, + data.lossHistory, ); const stoppedLr = getDisplayMetric( - runtime.isTrainingRunning, - runtime.currentLearningRate, - runtime.lrHistory, + data.isTrainingRunning, + data.currentLearningRate, + data.lrHistory, ); - const stoppedGradNorm = runtime.isTrainingRunning - ? runtime.currentGradNorm - : (lastNonZeroValue(runtime.gradNormHistory) ?? runtime.currentGradNorm); + const stoppedGradNorm = data.isTrainingRunning + ? data.currentGradNorm + : (lastValue(data.gradNormHistory) ?? data.currentGradNorm); + + const cfgEpochs = isHistorical ? configOverride?.epochs : config.epochs; + const cfgBatchSize = isHistorical ? configOverride?.batchSize : config.batchSize; + const cfgLearningRate = isHistorical ? configOverride?.learningRate : config.learningRate; + const cfgMaxSteps = isHistorical ? configOverride?.maxSteps : config.maxSteps; + const cfgContextLength = isHistorical ? configOverride?.contextLength : config.contextLength; + const cfgWarmupSteps = isHistorical ? configOverride?.warmupSteps : config.warmupSteps; + const cfgOptimizerType = isHistorical ? configOverride?.optimizerType : config.optimizerType; + const cfgLoraRank = isHistorical ? configOverride?.loraRank : config.loraRank; + const cfgLoraAlpha = isHistorical ? configOverride?.loraAlpha : config.loraAlpha; + const cfgLoraDropout = isHistorical ? configOverride?.loraDropout : config.loraDropout; + const cfgLoraVariant = isHistorical ? configOverride?.loraVariant : config.loraVariant; const optimizerLabel = - OPTIMIZER_OPTIONS.find((o) => o.value === config.optimizerType)?.label ?? - config.optimizerType; + OPTIMIZER_OPTIONS.find((o) => o.value === cfgOptimizerType)?.label ?? + cfgOptimizerType; const configItems: ConfigGroup[] = [ { section: "Hyperparams", rows: [ - configRow("Epochs", config.epochs), - configRow("Batch size", config.batchSize), - configRow("Learning rate", config.learningRate), + configRow("Epochs", cfgEpochs), + configRow("Batch size", cfgBatchSize), + configRow("Learning rate", cfgLearningRate), configRow("Optimizer", optimizerLabel), - configRow("Max steps", config.maxSteps), - configRow("Context length", config.contextLength), - configRow("Warmup steps", config.warmupSteps), + configRow("Max steps", cfgMaxSteps), + configRow("Context length", cfgContextLength), + configRow("Warmup steps", cfgWarmupSteps), ], }, - ...(config.trainingMethod !== "full" + ...(data.trainingMethod !== "full" ? [ { section: "LoRA", rows: [ - configRow("Rank", config.loraRank), - configRow("Alpha", config.loraAlpha), - configRow("Dropout", config.loraDropout), - configRow("Variant", config.loraVariant), + configRow("Rank", cfgLoraRank), + configRow("Alpha", cfgLoraAlpha), + configRow("Dropout", cfgLoraDropout), + configRow("Variant", cfgLoraVariant), ], }, ] @@ -205,30 +199,34 @@ export function ProgressSection(): ReactElement { } title="Training Progress" - description={runtime.message || "Live training metrics"} + description={data.message || "Live training metrics"} accent="emerald" className="shadow-border border border-border/60 bg-card/90 ring-0 backdrop-blur-sm" headerAction={ - + isHistorical ? ( + + ) : ( + + ) } >
- {phaseLabel[runtime.phase]} + {phaseLabel[data.phase]} - Epoch {runtime.currentEpoch.toFixed(2)} + Epoch {formatNumber(data.currentEpoch, 2)} {pct}% complete @@ -238,22 +236,24 @@ export function ProgressSection(): ReactElement {
- Step {runtime.currentStep} / {runtime.totalSteps || "--"} + Step {data.currentStep} / {data.totalSteps || "--"} {pct}%
- + {!isHistorical && ( + + )} - {runtime.error && ( + {data.error && (

- {runtime.error} + {data.error}

)} @@ -262,97 +262,196 @@ export function ProgressSection(): ReactElement { label="Loss" valueClassName="text-2xl font-bold tracking-tight" > - {stoppedLoss.toFixed(4)} + {stoppedLoss != null ? stoppedLoss.toFixed(4) : "--"} - {stoppedLr.toExponential(2)} + {stoppedLr != null ? stoppedLr.toExponential(2) : "--"} {formatNumber(stoppedGradNorm, 3)} - {config.selectedModel ?? "--"} + {data.modelName || "--"} - {config.trainingMethod === "qlora" ? "QLoRA" : config.trainingMethod === "lora" ? "LoRA" : "Full"} + {data.trainingMethod === "qlora" ? "QLoRA" : data.trainingMethod === "lora" ? "LoRA" : "Full"}
Elapsed: {formatDuration(elapsed)} - ETA: {formatDuration(eta)} + {!isHistorical && ETA: {formatDuration(eta)}} {stepsPerSecond == null ? "-- steps/s" : `${stepsPerSecond.toFixed(2)} steps/s`} - {runtime.currentNumTokens != null && ( - Tokens: {runtime.currentNumTokens} + {data.currentNumTokens != null && ( + Tokens: {data.currentNumTokens} )}
-
-
-

- GPU Monitor -

- Live -
-
- - } - value={ - gpu.gpu_utilization_pct != null - ? `${gpu.gpu_utilization_pct}%` - : "--" - } - pct={gpu.gpu_utilization_pct ?? 0} - /> - - } - value={ - gpu.temperature_c != null ? `${gpu.temperature_c}°C` : "--" - } - pct={gpu.temperature_c ?? 0} - max={100} - /> - } - value={ - gpu.vram_used_gb != null && gpu.vram_total_gb != null - ? `${gpu.vram_used_gb} / ${gpu.vram_total_gb} GB` - : "--" - } - pct={gpu.vram_utilization_pct ?? 0} - /> - } - value={ - gpu.power_draw_w != null - ? gpu.power_limit_w != null - ? `${gpu.power_draw_w} / ${gpu.power_limit_w} W` - : `${gpu.power_draw_w} W` - : "--" - } - pct={gpu.power_utilization_pct ?? 0} - /> -
-
+ {!isHistorical && ( + + )}
); } +function LiveGpuPanel({ + isTrainingRunning, +}: { + isTrainingRunning: boolean; +}): ReactElement { + const gpu = useGpuUtilization(isTrainingRunning); + + return ( +
+
+

+ GPU Monitor +

+ Live +
+
+ + } + value={ + gpu.gpu_utilization_pct != null + ? `${gpu.gpu_utilization_pct}%` + : "--" + } + pct={gpu.gpu_utilization_pct ?? 0} + /> + + } + value={ + gpu.temperature_c != null ? `${gpu.temperature_c}°C` : "--" + } + pct={gpu.temperature_c ?? 0} + max={100} + /> + } + value={ + gpu.vram_used_gb != null && gpu.vram_total_gb != null + ? `${gpu.vram_used_gb} / ${gpu.vram_total_gb} GB` + : "--" + } + pct={gpu.vram_utilization_pct ?? 0} + /> + } + value={ + gpu.power_draw_w != null + ? gpu.power_limit_w != null + ? `${gpu.power_draw_w} / ${gpu.power_limit_w} W` + : `${gpu.power_draw_w} W` + : "--" + } + pct={gpu.power_utilization_pct ?? 0} + /> +
+
+ ); +} + +function LiveTrainingHeaderActions({ + configItems, + isTrainingRunning, + onOpenStopDialog, + stopDialogOpen, + stopRequested, + onSetStopRequested, +}: { + configItems: ConfigGroup[]; + isTrainingRunning: boolean; + onOpenStopDialog: (open: boolean) => void; + stopDialogOpen: boolean; + stopRequested: boolean; + onSetStopRequested: (v: boolean) => void; +}): ReactElement { + const { stopTrainingRun } = useTrainingActions(); + + const requestStop = async (saveCheckpoint: boolean) => { + onSetStopRequested(true); + onOpenStopDialog(false); + useTrainingRuntimeStore.getState().setStopRequested(true); + try { + const ok = await stopTrainingRun(saveCheckpoint); + if (!ok) { + onSetStopRequested(false); + } + } catch { + onSetStopRequested(false); + } + }; + + return ( + + ); +} + +function ConfigPopoverButton({ + configItems, +}: { + configItems: ConfigGroup[]; +}): ReactElement { + return ( + + + + + +
+

Training Config

+ {configItems.map((group) => ( +
+

+ {group.section} +

+ {group.rows.map(([label, value]) => ( +
+ {label} + + {value == null || value === "" ? "--" : String(value)} + +
+ ))} +
+ ))} +
+
+
+ ); +} + function TrainingHeaderActions({ configItems, isTrainingRunning, @@ -370,39 +469,7 @@ function TrainingHeaderActions({ }): ReactElement { return (
- - - - - -
-

Training Config

- {configItems.map((group) => ( -
-

- {group.section} -

- {group.rows.map(([label, value]) => ( -
- {label} - - {String(value)} - -
- ))} -
- ))} -
-
-
+ - )} -

Fine-tuning Studio

-

- {showTrainingView - ? runtimeMessage || "Training in progress" - : "Configure and start training"} -

+

{subtitle}

{!hasHydratedRuntime && isHydratingRuntime ? (
Loading training runtime...
- ) : showTrainingView ? ( - ) : ( -
- - - - -
+ +
+ {selectedHistoryRunId && activeTab === "history" && ( + + )} + + + Configure + + + Current Run + + History + +
+ + +
+ + + + +
+
+ + + + + + + {selectedHistoryRunId ? ( + + ) : ( + { + if (runId === currentJobId && isTrainingRunning) { + handleTabChange("current-run"); + } else { + setSelectedHistoryRunId(runId); + } + }} /> + )} + +
)}
diff --git a/studio/frontend/src/features/studio/training-view.tsx b/studio/frontend/src/features/studio/training-view.tsx deleted file mode 100644 index b995b2f528..0000000000 --- a/studio/frontend/src/features/studio/training-view.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -import { cn } from "@/lib/utils"; -import { useTrainingRuntimeStore } from "@/features/training"; -import type { ReactElement } from "react"; -import { useShallow } from "zustand/react/shallow"; -import { ChartsSection } from "./sections/charts-section"; -import { ProgressSection } from "./sections/progress-section"; -import { TrainingStartOverlay } from "./training-start-overlay"; - -export function TrainingView(): ReactElement { - const runtime = useTrainingRuntimeStore( - useShallow((state) => ({ - phase: state.phase, - message: state.message, - currentStep: state.currentStep, - firstStepReceived: state.firstStepReceived, - isStarting: state.isStarting, - })), - ); - - const isPreparingPhase = - runtime.phase === "downloading_model" || - runtime.phase === "downloading_dataset" || - runtime.phase === "loading_model" || - runtime.phase === "loading_dataset" || - runtime.phase === "configuring"; - const isWaitingForFirstStep = - runtime.phase === "training" && !runtime.firstStepReceived; - const showOverlay = - runtime.isStarting || - isPreparingPhase || - (isWaitingForFirstStep && runtime.currentStep <= 0); - - return ( -
-
-
- -
- -
- {showOverlay ? ( - - ) : null} -
- ); -} diff --git a/studio/frontend/src/features/training/api/history-api.ts b/studio/frontend/src/features/training/api/history-api.ts new file mode 100644 index 0000000000..8f279eb439 --- /dev/null +++ b/studio/frontend/src/features/training/api/history-api.ts @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { authFetch } from "@/features/auth"; +import type { + TrainingRunDeleteResponse, + TrainingRunDetailResponse, + TrainingRunListResponse, +} from "../types/history"; + +async function readError(response: Response): Promise { + try { + const payload = (await response.json()) as { detail?: string; message?: string }; + return payload.detail || payload.message || `Request failed (${response.status})`; + } catch { + return `Request failed (${response.status})`; + } +} + +async function parseJson(response: Response): Promise { + if (!response.ok) { + throw new Error(await readError(response)); + } + return (await response.json()) as T; +} + +export async function listTrainingRuns( + limit = 50, + offset = 0, + signal?: AbortSignal, +): Promise { + const response = await authFetch( + `/api/train/runs?limit=${limit}&offset=${offset}`, + { signal }, + ); + return parseJson(response); +} + +export async function getTrainingRun( + runId: string, + signal?: AbortSignal, +): Promise { + const response = await authFetch( + `/api/train/runs/${encodeURIComponent(runId)}`, + { signal }, + ); + return parseJson(response); +} + +export async function deleteTrainingRun( + runId: string, + signal?: AbortSignal, +): Promise { + const response = await authFetch( + `/api/train/runs/${encodeURIComponent(runId)}`, + { method: "DELETE", signal }, + ); + return parseJson(response); +} diff --git a/studio/frontend/src/features/training/index.ts b/studio/frontend/src/features/training/index.ts index 9a70e0a71c..af34b306cf 100644 --- a/studio/frontend/src/features/training/index.ts +++ b/studio/frontend/src/features/training/index.ts @@ -14,6 +14,14 @@ export { useDatasetPreviewDialogStore } from "./stores/dataset-preview-dialog-st export { uploadTrainingDataset } from "./api/datasets-api"; export { listLocalModels } from "./api/models-api"; export type { LocalModelInfo } from "./api/models-api"; -export type { TrainingPhase } from "./types/runtime"; +export type { TrainingPhase, TrainingViewData, TrainingSeriesPoint } from "./types/runtime"; +export type { + TrainingRunSummary, + TrainingRunListResponse, + TrainingRunMetrics, + TrainingRunDetailResponse, + TrainingRunDeleteResponse, +} from "./types/history"; +export { listTrainingRuns, getTrainingRun, deleteTrainingRun } from "./api/history-api"; export { parseYamlConfig, serializeConfigToYaml } from "./lib/yaml-config"; export { validateTrainingConfig } from "./lib/validation"; diff --git a/studio/frontend/src/features/training/types/history.ts b/studio/frontend/src/features/training/types/history.ts new file mode 100644 index 0000000000..8b89db539b --- /dev/null +++ b/studio/frontend/src/features/training/types/history.ts @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +export interface TrainingRunSummary { + id: string; + status: "running" | "completed" | "stopped" | "error"; + model_name: string; + dataset_name: string; + started_at: string; + ended_at: string | null; + total_steps: number | null; + final_step: number | null; + final_loss: number | null; + output_dir: string | null; + duration_seconds: number | null; + error_message: string | null; + loss_sparkline: number[] | null; +} + +export interface TrainingRunListResponse { + runs: TrainingRunSummary[]; + total: number; +} + +export interface TrainingRunMetrics { + step_history: number[]; + loss_history: number[]; + loss_step_history: number[]; + lr_history: number[]; + lr_step_history: number[]; + grad_norm_history: number[]; + grad_norm_step_history: number[]; + eval_loss_history: number[]; + eval_step_history: number[]; + final_epoch: number | null; + final_num_tokens: number | null; +} + +export interface TrainingRunDetailResponse { + run: TrainingRunSummary; + config: Record; + metrics: TrainingRunMetrics; +} + +export interface TrainingRunDeleteResponse { + status: string; + message: string; +} diff --git a/studio/frontend/src/features/training/types/runtime.ts b/studio/frontend/src/features/training/types/runtime.ts index 7669c8b2f3..1bf319a5d1 100644 --- a/studio/frontend/src/features/training/types/runtime.ts +++ b/studio/frontend/src/features/training/types/runtime.ts @@ -53,8 +53,8 @@ export interface TrainingProgressPayload { job_id: string; step: number; total_steps: number; - loss: number; - learning_rate: number; + loss: number | null; + learning_rate: number | null; progress_percent: number; epoch: number | null; elapsed_seconds: number | null; @@ -118,3 +118,32 @@ export interface TrainingRuntimeActions { } export type TrainingRuntimeStore = TrainingRuntimeState & TrainingRuntimeActions; + +export interface TrainingViewData { + // Current metrics (for ProgressSection) + phase: TrainingPhase; + currentStep: number; + totalSteps: number; + currentLoss: number | null; + currentLearningRate: number | null; + currentGradNorm: number | null; + currentEpoch: number | null; + currentNumTokens: number | null; + progressPercent: number; + elapsedSeconds: number | null; + etaSeconds: number | null; + evalEnabled: boolean; + message: string; + error: string | null; + isTrainingRunning: boolean; + + // Config summary + modelName: string; + trainingMethod: string; + + // Time-series (for ChartsSection) + lossHistory: TrainingSeriesPoint[]; + lrHistory: TrainingSeriesPoint[]; + gradNormHistory: TrainingSeriesPoint[]; + evalLossHistory: TrainingSeriesPoint[]; +} diff --git a/studio/frontend/src/lib/latex.ts b/studio/frontend/src/lib/latex.ts new file mode 100644 index 0000000000..954d0f7bc6 --- /dev/null +++ b/studio/frontend/src/lib/latex.ts @@ -0,0 +1,97 @@ +// Adapted from LibreChat's latex.ts +// https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts +// +// Escapes currency dollar signs so they are not misinterpreted as LaTeX math +// delimiters when singleDollarTextMath is enabled. + +/** + * Matches a single $ followed by a number pattern (currency), e.g.: + * $5, $1,000, $5.99, $100K, $3.5M + * + * Does NOT match: + * $$ (display math), \$ (already escaped), $\alpha (LaTeX command) + */ +const CURRENCY_REGEX = + /(? { + const regions: Array<[number, number]> = []; + + // Fenced code blocks: ```...``` + const fencedRe = /```[\s\S]*?```/g; + let match: RegExpExecArray | null; + while ((match = fencedRe.exec(content)) !== null) { + regions.push([match.index, match.index + match[0].length]); + } + + // Inline code: `...` (but not inside fenced blocks -- we filter below) + const inlineRe = /`[^`\n]+`/g; + while ((match = inlineRe.exec(content)) !== null) { + const start = match.index; + const end = start + match[0].length; + // Skip if this backtick span falls inside a fenced block + let inside = false; + for (const [rs, re] of regions) { + if (start >= rs && end <= re) { + inside = true; + break; + } + } + if (!inside) { + regions.push([start, end]); + } + } + + // Sort by start position for binary search + regions.sort((a, b) => a[0] - b[0]); + return regions; +} + +/** + * Binary search to check if a position falls inside any code region. + */ +function isInCodeBlock( + position: number, + regions: Array<[number, number]>, +): boolean { + let lo = 0; + let hi = regions.length - 1; + while (lo <= hi) { + const mid = (lo + hi) >>> 1; + const [start, end] = regions[mid]; + if (position < start) { + hi = mid - 1; + } else if (position >= end) { + lo = mid + 1; + } else { + return true; + } + } + return false; +} + +/** + * Preprocess a markdown string to escape currency dollar signs so they are not + * parsed as LaTeX math delimiters. + * + * - `$5` alone becomes `\$5` (currency, not math) + * - `$\alpha$` is untouched (real LaTeX) + * - `$$E = mc^2$$` is untouched (display math) + * - Currency inside code blocks/spans is untouched + */ +export function preprocessLaTeX(content: string): string { + if (!content.includes("$")) return content; + + const codeRegions = findCodeBlockRegions(content); + + return content.replace(CURRENCY_REGEX, (match, offset) => { + if (isInCodeBlock(offset, codeRegions)) { + return match; + } + return "\\" + match; + }); +} diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py new file mode 100755 index 0000000000..516dc4b6a4 --- /dev/null +++ b/studio/install_llama_prebuilt.py @@ -0,0 +1,3427 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Cross platform llama.cpp prebuilt installer for Unsloth Studio""" + +from __future__ import annotations + +import argparse +import fnmatch +import hashlib +import json +import os +import platform +import random +import shutil +import site +import socket +import subprocess +import sys +import tarfile +import tempfile +import textwrap +import time +import urllib.error +import urllib.parse +import urllib.request +import zipfile +from contextlib import contextmanager +from dataclasses import dataclass + +try: + from filelock import FileLock, Timeout as FileLockTimeout +except ImportError: + FileLock = None + FileLockTimeout = None +from pathlib import Path +from typing import Any, Iterable, Iterator + + +EXIT_SUCCESS = 0 +EXIT_FALLBACK = 2 +EXIT_ERROR = 1 + +APPROVED_PREBUILT_LLAMA_TAG = "b8508" +DEFAULT_LLAMA_TAG = os.environ.get("UNSLOTH_LLAMA_TAG", APPROVED_PREBUILT_LLAMA_TAG) +DEFAULT_PUBLISHED_REPO = os.environ.get( + "UNSLOTH_LLAMA_RELEASE_REPO", "unslothai/llama.cpp" +) +DEFAULT_PUBLISHED_TAG = os.environ.get("UNSLOTH_LLAMA_RELEASE_TAG") +DEFAULT_PUBLISHED_MANIFEST_ASSET = os.environ.get( + "UNSLOTH_LLAMA_RELEASE_MANIFEST_ASSET", "llama-prebuilt-manifest.json" +) +DEFAULT_PUBLISHED_SHA256_ASSET = os.environ.get( + "UNSLOTH_LLAMA_RELEASE_SHA256_ASSET", "llama-prebuilt-sha256.json" +) +UPSTREAM_REPO = "ggml-org/llama.cpp" +UPSTREAM_RELEASES_API = f"https://api.github.com/repos/{UPSTREAM_REPO}/releases/latest" +TEST_MODEL_URL = ( + "https://huggingface.co/ggml-org/models/resolve/main/tinyllamas/stories260K.gguf" +) +TEST_MODEL_SHA256 = "270cba1bd5109f42d03350f60406024560464db173c0e387d91f0426d3bd256d" +VALIDATION_MODEL_CACHE_DIRNAME = ".cache" +VALIDATION_MODEL_CACHE_FILENAME = "stories260K.gguf" +INSTALL_LOCK_TIMEOUT_SECONDS = 300 +INSTALL_STAGING_ROOT_NAME = ".staging" +GITHUB_AUTH_HOSTS = {"api.github.com", "github.com"} +RETRYABLE_HTTP_STATUS = {408, 429, 500, 502, 503, 504} +HTTP_FETCH_ATTEMPTS = 4 +HTTP_FETCH_BASE_DELAY_SECONDS = 0.75 +SERVER_PORT_BIND_ATTEMPTS = 3 +SERVER_BIND_RETRY_WINDOW_SECONDS = 5.0 +TTY_PROGRESS_START_DELAY_SECONDS = 0.5 + + +@dataclass +class HostInfo: + system: str + machine: str + is_windows: bool + is_linux: bool + is_macos: bool + is_x86_64: bool + is_arm64: bool + nvidia_smi: str | None + driver_cuda_version: tuple[int, int] | None + compute_caps: list[str] + visible_cuda_devices: str | None + has_physical_nvidia: bool + has_usable_nvidia: bool + + +@dataclass +class AssetChoice: + repo: str + tag: str + name: str + url: str + source_label: str + runtime_name: str | None = None + runtime_url: str | None = None + is_ready_bundle: bool = False + install_kind: str = "" + bundle_profile: str | None = None + runtime_line: str | None = None + coverage_class: str | None = None + supported_sms: list[str] | None = None + min_sm: int | None = None + max_sm: int | None = None + selection_log: list[str] | None = None + expected_sha256: str | None = None + + +@dataclass(frozen = True) +class PublishedLlamaArtifact: + asset_name: str + install_kind: str + runtime_line: str | None + coverage_class: str | None + supported_sms: list[str] + min_sm: int | None + max_sm: int | None + bundle_profile: str | None + rank: int + + +@dataclass +class PublishedReleaseBundle: + repo: str + release_tag: str + upstream_tag: str + assets: dict[str, str] + manifest_asset_name: str + artifacts: list[PublishedLlamaArtifact] + selection_log: list[str] + + +@dataclass +class LinuxCudaSelection: + attempts: list[AssetChoice] + selection_log: list[str] + + @property + def primary(self) -> AssetChoice: + if not self.attempts: + raise RuntimeError("linux CUDA selection unexpectedly had no attempts") + return self.attempts[0] + + +@dataclass +class CudaRuntimePreference: + runtime_line: str | None + selection_log: list[str] + + +@dataclass(frozen = True) +class ApprovedArtifactHash: + asset_name: str + sha256: str + repo: str | None + kind: str | None + + +@dataclass +class ApprovedReleaseChecksums: + repo: str + release_tag: str + upstream_tag: str + source_commit: str | None + artifacts: dict[str, ApprovedArtifactHash] + + +class PrebuiltFallback(RuntimeError): + pass + + +def log(message: str) -> None: + print(f"[llama-prebuilt] {message}") + + +def log_lines(lines: Iterable[str]) -> None: + for line in lines: + log(line) + + +def parsed_hostname(url: str | None) -> str | None: + if not url: + return None + try: + hostname = urllib.parse.urlparse(url).hostname + except Exception: + return None + if not hostname: + return None + return hostname.lower() + + +def should_send_github_auth(url: str | None) -> bool: + return parsed_hostname(url) in GITHUB_AUTH_HOSTS + + +def auth_headers(url: str | None = None) -> dict[str, str]: + headers = { + "User-Agent": "unsloth-studio-llama-prebuilt", + } + token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") + if token and should_send_github_auth(url): + headers["Authorization"] = f"Bearer {token}" + return headers + + +def github_api_headers(url: str | None = None) -> dict[str, str]: + return { + "Accept": "application/vnd.github+json", + **auth_headers(url), + } + + +def is_github_api_url(url: str | None) -> bool: + return parsed_hostname(url) == "api.github.com" + + +def is_retryable_url_error(exc: Exception) -> bool: + if isinstance(exc, urllib.error.HTTPError): + return exc.code in RETRYABLE_HTTP_STATUS + if isinstance(exc, urllib.error.URLError): + return True + if isinstance(exc, TimeoutError): + return True + if isinstance(exc, socket.timeout): + return True + return False + + +def sleep_backoff( + attempt: int, *, base_delay: float = HTTP_FETCH_BASE_DELAY_SECONDS +) -> None: + delay = base_delay * (2 ** max(attempt - 1, 0)) + delay += random.uniform(0.0, 0.2) + time.sleep(delay) + + +def atomic_write_bytes(destination: Path, data: bytes) -> None: + destination.parent.mkdir(parents = True, exist_ok = True) + with tempfile.NamedTemporaryFile( + prefix = destination.name + ".tmp-", + dir = destination.parent, + delete = False, + ) as handle: + tmp_path = Path(handle.name) + handle.write(data) + handle.flush() + os.fsync(handle.fileno()) + os.replace(tmp_path, destination) + + +def atomic_replace_from_tempfile(tmp_path: Path, destination: Path) -> None: + destination.parent.mkdir(parents = True, exist_ok = True) + os.replace(tmp_path, destination) + + +def source_archive_logical_name(upstream_tag: str) -> str: + return f"llama.cpp-source-{upstream_tag}.tar.gz" + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def normalize_sha256_digest(value: str | None) -> str | None: + if not isinstance(value, str) or not value: + return None + lowered = value.lower() + if lowered.startswith("sha256:"): + lowered = lowered.split(":", 1)[1] + if len(lowered) != 64 or any(ch not in "0123456789abcdef" for ch in lowered): + return None + return lowered + + +def format_byte_count(num_bytes: float) -> str: + units = ["B", "KiB", "MiB", "GiB", "TiB"] + value = float(num_bytes) + for unit in units: + if abs(value) < 1024.0 or unit == units[-1]: + if unit == "B": + return f"{int(value)} {unit}" + return f"{value:.1f} {unit}" + value /= 1024.0 + return f"{num_bytes:.1f} B" + + +class DownloadProgress: + def __init__(self, label: str, total_bytes: int | None) -> None: + self.label = label + self.total_bytes = total_bytes if total_bytes and total_bytes > 0 else None + self.start_time = time.monotonic() + self.last_emit = 0.0 + term_ok = os.environ.get("TERM", "").lower() != "dumb" + self.stream = ( + sys.stderr + if sys.stderr.isatty() + else sys.stdout + if sys.stdout.isatty() + else sys.stderr + ) + self.is_tty = term_ok and self.stream.isatty() + self.completed = False + self.last_milestone_percent = -1 + self.last_milestone_bytes = 0 + self.has_rendered_tty_progress = False + + def _render(self, downloaded_bytes: int, *, final: bool = False) -> str: + elapsed = max(time.monotonic() - self.start_time, 1e-6) + speed = downloaded_bytes / elapsed + speed_text = f"{format_byte_count(speed)}/s" + if self.total_bytes is not None: + percent = min(100.0, (downloaded_bytes / self.total_bytes) * 100.0) + return ( + f"{self.label}: {percent:5.1f}% " + f"({format_byte_count(downloaded_bytes)}/{format_byte_count(self.total_bytes)}) " + f"at {speed_text}" + ) + if final: + return f"{self.label}: {format_byte_count(downloaded_bytes)} downloaded at {speed_text}" + return f"{self.label}: {format_byte_count(downloaded_bytes)} downloaded at {speed_text}" + + def update(self, downloaded_bytes: int) -> None: + now = time.monotonic() + if self.is_tty: + elapsed = now - self.start_time + if not self.has_rendered_tty_progress: + if ( + self.total_bytes is not None + and downloaded_bytes >= self.total_bytes + ): + return + if elapsed < TTY_PROGRESS_START_DELAY_SECONDS: + return + min_interval = 0.2 + if ( + self.has_rendered_tty_progress + and not self.completed + and (now - self.last_emit) < min_interval + ): + return + self.last_emit = now + line = self._render(downloaded_bytes) + self.stream.write("\r\033[K" + line) + self.stream.flush() + self.has_rendered_tty_progress = True + return + + should_emit = False + if self.total_bytes is not None: + percent = int((downloaded_bytes * 100) / max(self.total_bytes, 1)) + milestone_percent = min((percent // 25) * 25, 100) + if ( + milestone_percent > self.last_milestone_percent + and milestone_percent < 100 + ): + self.last_milestone_percent = milestone_percent + should_emit = True + else: + byte_step = 25 * 1024 * 1024 + if ( + downloaded_bytes - self.last_milestone_bytes >= byte_step + and (now - self.last_emit) >= 5.0 + ): + self.last_milestone_bytes = downloaded_bytes + should_emit = True + + if not should_emit: + return + + self.last_emit = now + self.stream.write(self._render(downloaded_bytes) + "\n") + self.stream.flush() + + def finish(self, downloaded_bytes: int) -> None: + self.completed = True + line = self._render(downloaded_bytes, final = True) + if self.is_tty: + if not self.has_rendered_tty_progress: + return + self.stream.write("\r\033[K") + else: + self.stream.write(line + "\n") + self.stream.flush() + + +def download_label_from_url(url: str) -> str: + name = Path(urllib.parse.urlparse(url).path).name + return name or url + + +def download_bytes( + url: str, + *, + timeout: int = 120, + attempts: int = HTTP_FETCH_ATTEMPTS, + headers: dict[str, str] | None = None, + progress_label: str | None = None, +) -> bytes: + last_exc: Exception | None = None + for attempt in range(1, attempts + 1): + try: + request = urllib.request.Request(url, headers = headers or auth_headers(url)) + with urllib.request.urlopen(request, timeout = timeout) as response: + total_bytes: int | None = None + content_length = response.headers.get("Content-Length") + if content_length and content_length.isdigit(): + total_bytes = int(content_length) + progress = ( + DownloadProgress(progress_label, total_bytes) + if progress_label + else None + ) + data = bytearray() + while True: + chunk = response.read(1024 * 1024) + if not chunk: + break + data.extend(chunk) + if progress is not None: + progress.update(len(data)) + if progress is not None: + progress.finish(len(data)) + return bytes(data) + except Exception as exc: + last_exc = exc + if attempt >= attempts or not is_retryable_url_error(exc): + raise + log(f"fetch failed ({attempt}/{attempts}) for {url}: {exc}; retrying") + sleep_backoff(attempt) + assert last_exc is not None + raise last_exc + + +def fetch_json(url: str) -> Any: + data = download_bytes( + url, + timeout = 30, + headers = github_api_headers(url) + if is_github_api_url(url) + else auth_headers(url), + ) + if not data: + raise RuntimeError(f"downloaded empty JSON payload from {url}") + try: + payload = json.loads(data.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise RuntimeError(f"downloaded invalid JSON from {url}: {exc}") from exc + if not isinstance(payload, dict) and not isinstance(payload, list): + raise RuntimeError( + f"downloaded unexpected JSON type from {url}: {type(payload).__name__}" + ) + return payload + + +def download_file(url: str, destination: Path) -> None: + destination.parent.mkdir(parents = True, exist_ok = True) + last_exc: Exception | None = None + for attempt in range(1, HTTP_FETCH_ATTEMPTS + 1): + tmp_path: Path | None = None + try: + request = urllib.request.Request(url, headers = auth_headers(url)) + with tempfile.NamedTemporaryFile( + prefix = destination.name + ".tmp-", + dir = destination.parent, + delete = False, + ) as handle: + tmp_path = Path(handle.name) + with urllib.request.urlopen(request, timeout = 120) as response: + total_bytes: int | None = None + content_length = response.headers.get("Content-Length") + if content_length and content_length.isdigit(): + total_bytes = int(content_length) + progress = DownloadProgress( + f"Downloading {destination.name}", total_bytes + ) + downloaded_bytes = 0 + while True: + chunk = response.read(1024 * 1024) + if not chunk: + break + handle.write(chunk) + downloaded_bytes += len(chunk) + progress.update(downloaded_bytes) + progress.finish(downloaded_bytes) + handle.flush() + os.fsync(handle.fileno()) + if not tmp_path.exists() or tmp_path.stat().st_size == 0: + raise RuntimeError(f"downloaded empty file from {url}") + atomic_replace_from_tempfile(tmp_path, destination) + return + except Exception as exc: + last_exc = exc + if tmp_path is not None: + try: + tmp_path.unlink(missing_ok = True) + except Exception: + pass + if attempt >= HTTP_FETCH_ATTEMPTS or not is_retryable_url_error(exc): + raise + log( + f"download failed ({attempt}/{HTTP_FETCH_ATTEMPTS}) for {url}: {exc}; retrying" + ) + sleep_backoff(attempt) + assert last_exc is not None + raise last_exc + + +def download_file_verified( + url: str, + destination: Path, + *, + expected_sha256: str, + label: str, +) -> None: + normalized_expected = normalize_sha256_digest(expected_sha256) + if not normalized_expected: + raise PrebuiltFallback(f"{label} did not have a valid approved sha256") + + for attempt in range(1, 3): + download_file(url, destination) + actual_sha256 = sha256_file(destination) + if actual_sha256 == normalized_expected: + log(f"verified {label} sha256={actual_sha256}") + return + + log( + f"{label} checksum mismatch on attempt {attempt}/2: " + f"expected={normalized_expected} actual={actual_sha256}" + ) + destination.unlink(missing_ok = True) + if attempt == 2: + raise PrebuiltFallback( + f"{label} checksum mismatch after retry: expected={normalized_expected} actual={actual_sha256}" + ) + log(f"retrying {label} download after checksum mismatch") + + +def upstream_source_archive_urls(tag: str) -> list[str]: + encoded_tag = urllib.parse.quote(tag, safe = "") + return [ + f"https://codeload.github.com/{UPSTREAM_REPO}/tar.gz/refs/tags/{encoded_tag}", + f"https://github.com/{UPSTREAM_REPO}/archive/refs/tags/{encoded_tag}.tar.gz", + ] + + +def github_release_assets(repo: str, tag: str) -> dict[str, str]: + payload = fetch_json( + f"https://api.github.com/repos/{repo}/releases/tags/{urllib.parse.quote(tag, safe = '')}" + ) + if not isinstance(payload, dict): + raise RuntimeError(f"unexpected release payload for {repo}@{tag}") + return release_asset_map(payload) + + +def github_release(repo: str, tag: str) -> dict[str, Any]: + payload = fetch_json( + f"https://api.github.com/repos/{repo}/releases/tags/{urllib.parse.quote(tag, safe = '')}" + ) + if not isinstance(payload, dict): + raise RuntimeError(f"unexpected release payload for {repo}@{tag}") + return payload + + +def github_releases(repo: str, *, per_page: int = 100) -> list[dict[str, Any]]: + releases: list[dict[str, Any]] = [] + page = 1 + while True: + payload = fetch_json( + f"https://api.github.com/repos/{repo}/releases?per_page={per_page}&page={page}" + ) + if not isinstance(payload, list): + raise RuntimeError(f"unexpected releases payload for {repo}") + page_items = [item for item in payload if isinstance(item, dict)] + releases.extend(page_items) + if len(payload) < per_page: + break + page += 1 + return releases + + +def latest_upstream_release_tag() -> str: + payload = fetch_json(UPSTREAM_RELEASES_API) + tag = payload.get("tag_name") + if not isinstance(tag, str) or not tag: + raise RuntimeError( + f"latest release tag was missing from {UPSTREAM_RELEASES_API}" + ) + return tag + + +def normalize_compute_cap(value: Any) -> str | None: + raw = str(value).strip() + if not raw: + return None + if "." in raw: + parts = raw.split(".", 1) + if len(parts) != 2: + return None + major, minor = parts + if not major.isdigit() or not minor.isdigit(): + return None + return f"{int(major)}{int(minor)}" + if raw.isdigit(): + return str(int(raw)) + return None + + +def normalize_compute_caps(compute_caps: Iterable[str]) -> list[str]: + normalized: list[str] = [] + seen: set[str] = set() + for raw in compute_caps: + normalized_value = normalize_compute_cap(raw) + if normalized_value is None: + continue + if normalized_value in seen: + continue + seen.add(normalized_value) + normalized.append(normalized_value) + normalized.sort(key = int) + return normalized + + +def parse_cuda_visible_devices(value: str | None) -> list[str] | None: + if value is None: + return None + raw = value.strip() + if not raw or raw == "-1": + return [] + return [token.strip() for token in raw.split(",") if token.strip()] + + +def supports_explicit_visible_device_matching( + visible_devices: list[str] | None, +) -> bool: + if not visible_devices: + return False + for token in visible_devices: + lowered = token.lower() + if token.isdigit() or lowered.startswith("gpu-"): + continue + return False + return True + + +def select_visible_gpu_rows( + gpu_rows: Iterable[tuple[str, str, str]], + visible_devices: list[str] | None, +) -> list[tuple[str, str, str]]: + rows = list(gpu_rows) + if visible_devices is None: + return rows + if not visible_devices: + return [] + + by_index = {index: (index, uuid, cap) for index, uuid, cap in rows} + by_uuid = {uuid.lower(): (index, uuid, cap) for index, uuid, cap in rows} + selected: list[tuple[str, str, str]] = [] + seen_indices: set[str] = set() + for token in visible_devices: + row = by_index.get(token) + if row is None: + normalized_token = token.lower() + row = by_uuid.get(normalized_token) + if row is None and normalized_token.startswith("gpu-"): + row = by_uuid.get(normalized_token) + if row is None and not normalized_token.startswith("gpu-"): + row = by_uuid.get("gpu-" + normalized_token) + if row is None: + continue + index = row[0] + if index in seen_indices: + continue + seen_indices.add(index) + selected.append(row) + return selected + + +def dir_provides_exact_library(directory: str | Path, library: str) -> bool: + if not library: + return False + candidate = Path(directory) / library + return candidate.exists() and (candidate.is_file() or candidate.is_symlink()) + + +def linux_runtime_dirs_for_required_libraries( + required_libraries: Iterable[str], +) -> list[str]: + required = [library for library in required_libraries if library] + candidates: list[str | Path] = [] + + env_dirs = os.environ.get("CUDA_RUNTIME_LIB_DIR", "") + if env_dirs: + candidates.extend(part for part in env_dirs.split(os.pathsep) if part) + ld_library_path = os.environ.get("LD_LIBRARY_PATH", "") + if ld_library_path: + candidates.extend(part for part in ld_library_path.split(os.pathsep) if part) + + cuda_roots: list[Path] = [] + for name in ("CUDA_HOME", "CUDA_PATH", "CUDA_ROOT"): + value = os.environ.get(name) + if value: + cuda_roots.append(Path(value)) + cuda_roots.extend( + Path(path) for path in glob_paths("/usr/local/cuda", "/usr/local/cuda-*") + ) + + for root in cuda_roots: + candidates.extend( + [ + root / "lib", + root / "lib64", + root / "targets" / "x86_64-linux" / "lib", + ] + ) + + candidates.extend( + Path(path) + for path in glob_paths( + "/lib", + "/lib64", + "/usr/lib", + "/usr/lib64", + "/usr/local/lib", + "/usr/local/lib64", + "/lib/x86_64-linux-gnu", + "/usr/lib/x86_64-linux-gnu", + ) + ) + candidates.extend( + Path(path) + for path in glob_paths("/usr/local/lib/ollama/cuda_v*", "/usr/lib/wsl/lib") + ) + candidates.extend(Path(path) for path in python_runtime_dirs()) + candidates.extend(Path(path) for path in ldconfig_runtime_dirs(required)) + + resolved = dedupe_existing_dirs(candidates) + if not required: + return resolved + + matched: list[tuple[int, str]] = [] + for directory in resolved: + base = Path(directory) + provided = sum( + 1 for library in required if dir_provides_exact_library(directory, library) + ) + if provided: + matched.append((provided, directory)) + + matched.sort(key = lambda item: item[0], reverse = True) + return [directory for _, directory in matched] + + +def detected_linux_runtime_lines() -> tuple[list[str], dict[str, list[str]]]: + line_requirements = { + "cuda13": ["libcudart.so.13", "libcublas.so.13"], + "cuda12": ["libcudart.so.12", "libcublas.so.12"], + } + detected: list[str] = [] + runtime_dirs: dict[str, list[str]] = {} + for line, required in line_requirements.items(): + dirs = linux_runtime_dirs_for_required_libraries(required) + library_matches: dict[str, list[str]] = {} + matching_dirs: list[str] = [] + for library in required: + matched_dirs = [ + directory + for directory in dirs + if any(Path(directory).glob(f"{library}*")) + ] + if not matched_dirs: + library_matches = {} + matching_dirs = [] + break + library_matches[library] = matched_dirs + for directory in matched_dirs: + if directory not in matching_dirs: + matching_dirs.append(directory) + if library_matches: + detected.append(line) + runtime_dirs[line] = matching_dirs + return detected, runtime_dirs + + +def release_asset_map(release: dict[str, Any]) -> dict[str, str]: + assets = release.get("assets") + if not isinstance(assets, list): + return {} + return { + asset["name"]: asset.get("browser_download_url", "") + for asset in assets + if isinstance(asset, dict) + and isinstance(asset.get("name"), str) + and isinstance(asset.get("browser_download_url"), str) + } + + +def parse_published_artifact(raw: Any) -> PublishedLlamaArtifact | None: + if not isinstance(raw, dict): + raise ValueError("artifact entry was not an object") + asset_name = raw.get("asset_name") + install_kind = raw.get("install_kind") + if not isinstance(asset_name, str) or not asset_name: + raise ValueError("artifact.asset_name was missing or not a string") + if not isinstance(install_kind, str) or not install_kind: + raise ValueError( + f"artifact {asset_name} install_kind was missing or not a string" + ) + + supported_sms_raw = raw.get("supported_sms", []) + if not isinstance(supported_sms_raw, (list, tuple)): + raise ValueError(f"artifact {asset_name} supported_sms must be a list or tuple") + if any(not isinstance(value, (int, str)) for value in supported_sms_raw): + raise ValueError( + f"artifact {asset_name} supported_sms entries must be ints or strings" + ) + supported_sms = normalize_compute_caps(supported_sms_raw) + + min_sm_raw = raw.get("min_sm") + max_sm_raw = raw.get("max_sm") + try: + min_sm = int(min_sm_raw) if min_sm_raw is not None else None + max_sm = int(max_sm_raw) if max_sm_raw is not None else None + except (TypeError, ValueError) as exc: + raise ValueError( + f"artifact {asset_name} min_sm/max_sm were not integers" + ) from exc + runtime_line = raw.get("runtime_line") + coverage_class = raw.get("coverage_class") + bundle_profile = raw.get("bundle_profile") + rank_raw = raw.get("rank", 1000) + if runtime_line is not None and not isinstance(runtime_line, str): + raise ValueError(f"artifact {asset_name} runtime_line was not a string") + if coverage_class is not None and not isinstance(coverage_class, str): + raise ValueError(f"artifact {asset_name} coverage_class was not a string") + if bundle_profile is not None and not isinstance(bundle_profile, str): + raise ValueError(f"artifact {asset_name} bundle_profile was not a string") + try: + rank = int(rank_raw) + except (TypeError, ValueError): + raise ValueError(f"artifact {asset_name} rank was not an integer") + return PublishedLlamaArtifact( + asset_name = asset_name, + install_kind = install_kind, + runtime_line = runtime_line + if isinstance(runtime_line, str) and runtime_line + else None, + coverage_class = coverage_class + if isinstance(coverage_class, str) and coverage_class + else None, + supported_sms = supported_sms, + min_sm = min_sm, + max_sm = max_sm, + bundle_profile = bundle_profile + if isinstance(bundle_profile, str) and bundle_profile + else None, + rank = rank, + ) + + +def parse_published_release_bundle( + repo: str, release: dict[str, Any] +) -> PublishedReleaseBundle | None: + release_tag = release.get("tag_name") + if not isinstance(release_tag, str) or not release_tag: + return None + + assets = release_asset_map(release) + manifest_url = assets.get(DEFAULT_PUBLISHED_MANIFEST_ASSET) + if not manifest_url: + return None + + # Mixed repos are filtered by an explicit release-side manifest rather than + # by release tag or asset filename conventions. + manifest_payload = fetch_json(manifest_url) + if not isinstance(manifest_payload, dict): + raise RuntimeError( + f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} was not a JSON object" + ) + component = manifest_payload.get("component") + upstream_tag = manifest_payload.get("upstream_tag") + if component != "llama.cpp": + return None + if not isinstance(upstream_tag, str) or not upstream_tag: + raise RuntimeError( + f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} in {repo}@{release_tag} omitted upstream_tag" + ) + + artifacts_payload = manifest_payload.get("artifacts") + if not isinstance(artifacts_payload, list): + raise RuntimeError( + f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} in {repo}@{release_tag} omitted artifacts" + ) + + artifacts: list[PublishedLlamaArtifact] = [] + for index, raw_artifact in enumerate(artifacts_payload): + try: + artifact = parse_published_artifact(raw_artifact) + except ValueError as exc: + log( + f"published artifact ignored for {repo}@{release_tag} artifact[{index}]: {exc}" + ) + continue + if artifact is not None: + artifacts.append(artifact) + selection_log = [ + f"published_release: repo={repo}", + f"published_release: tag={release_tag}", + f"published_release: manifest={DEFAULT_PUBLISHED_MANIFEST_ASSET}", + f"published_release: upstream_tag={upstream_tag}", + ] + return PublishedReleaseBundle( + repo = repo, + release_tag = release_tag, + upstream_tag = upstream_tag, + assets = assets, + manifest_asset_name = DEFAULT_PUBLISHED_MANIFEST_ASSET, + artifacts = artifacts, + selection_log = selection_log, + ) + + +def parse_approved_release_checksums( + repo: str, + release_tag: str, + payload: Any, +) -> ApprovedReleaseChecksums: + if not isinstance(payload, dict): + raise RuntimeError( + f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} was not a JSON object" + ) + if payload.get("component") != "llama.cpp": + raise RuntimeError( + f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} did not describe llama.cpp" + ) + payload_release_tag = payload.get("release_tag") + if not isinstance(payload_release_tag, str) or not payload_release_tag: + raise RuntimeError( + f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} omitted release_tag" + ) + if payload_release_tag != release_tag: + raise RuntimeError( + f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} release_tag={payload_release_tag} " + f"did not match pinned release tag {release_tag}" + ) + upstream_tag = payload.get("upstream_tag") + if not isinstance(upstream_tag, str) or not upstream_tag: + raise RuntimeError( + f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} omitted upstream_tag" + ) + artifacts_payload = payload.get("artifacts") + if not isinstance(artifacts_payload, dict): + raise RuntimeError( + f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} omitted artifacts" + ) + + artifacts: dict[str, ApprovedArtifactHash] = {} + for asset_name, raw_entry in artifacts_payload.items(): + if not isinstance(asset_name, str) or not asset_name: + raise RuntimeError( + "published checksum asset used a non-string artifact key" + ) + if not isinstance(raw_entry, dict): + raise RuntimeError( + f"published checksum entry for {asset_name} was not an object" + ) + digest = normalize_sha256_digest(raw_entry.get("sha256")) + if not digest: + raise RuntimeError( + f"published checksum entry for {asset_name} omitted a valid sha256" + ) + repo_value = raw_entry.get("repo") + kind_value = raw_entry.get("kind") + artifacts[asset_name] = ApprovedArtifactHash( + asset_name = asset_name, + sha256 = digest, + repo = repo_value if isinstance(repo_value, str) and repo_value else None, + kind = kind_value if isinstance(kind_value, str) and kind_value else None, + ) + + source_commit = payload.get("source_commit") + return ApprovedReleaseChecksums( + repo = repo, + release_tag = release_tag, + upstream_tag = upstream_tag, + source_commit = source_commit + if isinstance(source_commit, str) and source_commit + else None, + artifacts = artifacts, + ) + + +def load_approved_release_checksums( + repo: str, release_tag: str +) -> ApprovedReleaseChecksums: + try: + release = github_release(repo, release_tag) + except Exception as exc: + raise PrebuiltFallback( + f"approved prebuilt release {repo}@{release_tag} was not available" + ) from exc + assets = release_asset_map(release) + checksum_url = assets.get(DEFAULT_PUBLISHED_SHA256_ASSET) + if not checksum_url: + raise PrebuiltFallback( + f"approved prebuilt release {repo}@{release_tag} did not expose {DEFAULT_PUBLISHED_SHA256_ASSET}" + ) + try: + payload = fetch_json(checksum_url) + checksums = parse_approved_release_checksums(repo, release_tag, payload) + except PrebuiltFallback: + raise + except Exception as exc: + raise PrebuiltFallback( + f"approved checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} in {repo}@{release_tag} was invalid" + ) from exc + return checksums + + +def iter_published_release_bundles( + repo: str, published_release_tag: str = "" +) -> Iterable[PublishedReleaseBundle]: + releases = ( + [github_release(repo, published_release_tag)] + if published_release_tag + else github_releases(repo) + ) + for release in releases: + if not published_release_tag and ( + release.get("draft") or release.get("prerelease") + ): + continue + try: + bundle = parse_published_release_bundle(repo, release) + except Exception as exc: + release_tag = release.get("tag_name", "unknown") + log(f"published release metadata ignored for {repo}@{release_tag}: {exc}") + continue + if bundle is None: + continue + yield bundle + + +def linux_cuda_choice_from_release( + host: HostInfo, + release: PublishedReleaseBundle, + preferred_runtime_line: str | None = None, + selection_preamble: Iterable[str] = (), +) -> LinuxCudaSelection | None: + host_sms = normalize_compute_caps(host.compute_caps) + detected_runtime_lines, runtime_dirs = detected_linux_runtime_lines() + driver_runtime_lines = compatible_linux_runtime_lines(host) + runtime_lines = [ + runtime_line + for runtime_line in detected_runtime_lines + if runtime_line in driver_runtime_lines + ] + ordered_runtime_lines = list(runtime_lines) + selection_log = ( + list(release.selection_log) + + list(selection_preamble) + + [ + f"linux_cuda_selection: release={release.release_tag}", + f"linux_cuda_selection: detected_sms={','.join(host_sms) if host_sms else 'unknown'}", + "linux_cuda_selection: detected_runtime_lines=" + + (",".join(detected_runtime_lines) if detected_runtime_lines else "none"), + "linux_cuda_selection: driver_runtime_lines=" + + (",".join(driver_runtime_lines) if driver_runtime_lines else "none"), + "linux_cuda_selection: compatible_runtime_lines=" + + (",".join(runtime_lines) if runtime_lines else "none"), + ] + ) + for runtime_line in ("cuda13", "cuda12"): + selection_log.append( + "linux_cuda_selection: runtime_dirs " + f"{runtime_line}=" + + ( + ",".join(runtime_dirs.get(runtime_line, [])) + if runtime_dirs.get(runtime_line) + else "none" + ) + ) + published_artifacts = [ + artifact + for artifact in release.artifacts + if artifact.install_kind == "linux-cuda" + ] + published_asset_names = sorted( + artifact.asset_name for artifact in published_artifacts + ) + selection_log.append( + "linux_cuda_selection: published_assets=" + + (",".join(published_asset_names) if published_asset_names else "none") + ) + + if not host_sms: + selection_log.append( + "linux_cuda_selection: compute capability detection unavailable; prefer portable by runtime line" + ) + if not runtime_lines: + selection_log.append( + "linux_cuda_selection: no Linux CUDA runtime line satisfied both runtime libraries and driver compatibility" + ) + return None + + if preferred_runtime_line: + if preferred_runtime_line in ordered_runtime_lines: + ordered_runtime_lines = [preferred_runtime_line] + [ + runtime_line + for runtime_line in ordered_runtime_lines + if runtime_line != preferred_runtime_line + ] + selection_log.append( + "linux_cuda_selection: torch_preferred_runtime_line=" + f"{preferred_runtime_line} reordered_attempts={','.join(ordered_runtime_lines)}" + ) + else: + selection_log.append( + "linux_cuda_selection: torch_preferred_runtime_line=" + f"{preferred_runtime_line} unavailable_on_host" + ) + + attempts: list[AssetChoice] = [] + seen_attempts: set[str] = set() + + def add_attempt( + artifact: PublishedLlamaArtifact, asset_url: str, reason: str + ) -> None: + asset_name = artifact.asset_name + if asset_name in seen_attempts: + return + seen_attempts.add(asset_name) + attempts.append( + AssetChoice( + repo = release.repo, + tag = release.release_tag, + name = asset_name, + url = asset_url, + source_label = "published", + is_ready_bundle = True, + install_kind = "linux-cuda", + bundle_profile = artifact.bundle_profile, + runtime_line = artifact.runtime_line, + coverage_class = artifact.coverage_class, + supported_sms = artifact.supported_sms, + min_sm = artifact.min_sm, + max_sm = artifact.max_sm, + selection_log = list(selection_log) + + [ + "linux_cuda_selection: selected " + f"{asset_name} runtime_line={artifact.runtime_line} coverage_class={artifact.coverage_class} reason={reason}" + ], + ) + ) + + for runtime_line in ordered_runtime_lines: + coverage_candidates: list[tuple[PublishedLlamaArtifact, str]] = [] + portable_candidate: tuple[PublishedLlamaArtifact, str] | None = None + for artifact in published_artifacts: + if artifact.runtime_line != runtime_line: + continue + asset_name = artifact.asset_name + asset_url = release.assets.get(asset_name) + if not asset_url: + selection_log.append( + f"linux_cuda_selection: reject {asset_name} missing asset" + ) + continue + if not host_sms and artifact.coverage_class != "portable": + selection_log.append( + "linux_cuda_selection: reject " + f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} " + "reason=unknown_compute_caps_prefer_portable" + ) + continue + + if not artifact.supported_sms: + selection_log.append( + "linux_cuda_selection: reject " + f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} " + "reason=artifact_missing_supported_sms" + ) + continue + if artifact.min_sm is None or artifact.max_sm is None: + selection_log.append( + "linux_cuda_selection: reject " + f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} " + "reason=artifact_missing_sm_bounds" + ) + continue + + supported_sms = {str(value) for value in artifact.supported_sms} + missing_sms = [sm for sm in host_sms if sm not in supported_sms] + out_of_range_sms = [ + sm + for sm in host_sms + if not (artifact.min_sm <= int(sm) <= artifact.max_sm) + ] + reasons: list[str] = [] + if missing_sms: + reasons.append(f"missing_sms={','.join(missing_sms)}") + if out_of_range_sms: + reasons.append(f"out_of_range_sms={','.join(out_of_range_sms)}") + if reasons: + selection_log.append( + "linux_cuda_selection: reject " + f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} " + f"coverage={artifact.min_sm}-{artifact.max_sm} supported={','.join(artifact.supported_sms)} " + f"reasons={' '.join(reasons)}" + ) + continue + + selection_log.append( + "linux_cuda_selection: accept " + f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} " + f"coverage={artifact.min_sm}-{artifact.max_sm} supported={','.join(artifact.supported_sms)}" + ) + if artifact.coverage_class == "portable": + portable_candidate = (artifact, asset_url) + else: + coverage_candidates.append((artifact, asset_url)) + + if coverage_candidates: + artifact, url = sorted( + coverage_candidates, + key = lambda item: ( + (item[0].max_sm or 0) - (item[0].min_sm or 0), + item[0].rank, + item[0].max_sm or 0, + ), + )[0] + add_attempt(artifact, url, "best coverage for runtime line") + if portable_candidate: + artifact, url = portable_candidate + add_attempt(artifact, url, "portable fallback for runtime line") + + if not attempts: + return None + + selection_log.append( + "linux_cuda_selection: attempt_order=" + + ",".join(choice.name for choice in attempts) + ) + for attempt in attempts: + attempt.selection_log = list(selection_log) + [ + "linux_cuda_selection: attempt " + f"{attempt.name} runtime_line={attempt.runtime_line} coverage_class={attempt.coverage_class}" + ] + return LinuxCudaSelection(attempts = attempts, selection_log = selection_log) + + +def latest_published_linux_cuda_tag(host: HostInfo, published_repo: str) -> str | None: + for release in iter_published_release_bundles(published_repo): + if linux_cuda_choice_from_release(host, release): + return release.upstream_tag + return None + + +def iter_upstream_releases() -> Iterable[dict[str, Any]]: + for release in github_releases(UPSTREAM_REPO): + if release.get("draft") or release.get("prerelease"): + continue + yield release + + +def pinned_published_release_bundle( + repo: str, published_release_tag: str +) -> PublishedReleaseBundle: + bundle = next(iter_published_release_bundles(repo, published_release_tag), None) + if bundle is None: + raise PrebuiltFallback( + f"published release {repo}@{published_release_tag} did not expose a usable llama.cpp manifest" + ) + return bundle + + +def resolve_requested_llama_tag( + requested_tag: str | None, + published_repo: str = "", +) -> str: + """Resolve a llama.cpp tag for source-build fallback. + + Resolution order: + 1. Concrete tag (e.g. "b8508") -- returned as-is. + 2. "latest" with published_repo -- query the Unsloth release repo + (e.g. unslothai/llama.cpp) for its latest release tag. This is the + tested/approved version that matches the prebuilt binaries. + 3. "latest" without published_repo or if (2) fails -- query the upstream + ggml-org/llama.cpp repo. This may return a newer, untested tag. + + The Unsloth repo is preferred because its releases are pinned to specific + upstream tags that have been validated with Unsloth Studio. Using the + upstream bleeding-edge tag risks API/ABI incompatibilities. + """ + if requested_tag and requested_tag != "latest": + return requested_tag + # Prefer the Unsloth release repo tag (tested/approved) over bleeding-edge + # upstream. For example, unslothai/llama.cpp may publish b8508 while + # ggml-org/llama.cpp latest is b8514. The source-build fallback should + # compile the same version the prebuilt path would have installed. + if published_repo: + try: + payload = fetch_json( + f"https://api.github.com/repos/{published_repo}/releases/latest" + ) + tag = payload.get("tag_name") + if isinstance(tag, str) and tag: + return tag + except Exception: + pass + # Fall back to upstream ggml-org latest release tag + return latest_upstream_release_tag() + + +def resolve_requested_install_tag( + requested_tag: str | None, + published_release_tag: str = "", +) -> str: + approved_tag = APPROVED_PREBUILT_LLAMA_TAG + normalized_requested = requested_tag or "latest" + if normalized_requested not in {"latest", approved_tag}: + raise PrebuiltFallback( + f"prebuilt installs are pinned to approved release {approved_tag}; requested {normalized_requested}" + ) + if published_release_tag and published_release_tag != approved_tag: + raise PrebuiltFallback( + f"prebuilt installs require published release tag {approved_tag}; requested {published_release_tag}" + ) + return approved_tag + + +def run_capture( + command: list[str], + *, + timeout: int = 30, + check: bool = False, + env: dict[str, str] | None = None, +) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + command, + capture_output = True, + text = True, + timeout = timeout, + env = env, + ) + if check and result.returncode != 0: + raise subprocess.CalledProcessError( + result.returncode, command, result.stdout, result.stderr + ) + return result + + +def detect_host() -> HostInfo: + system = platform.system() + machine = platform.machine().lower() + is_windows = system == "Windows" + is_linux = system == "Linux" + is_macos = system == "Darwin" + is_x86_64 = machine in {"x86_64", "amd64"} + is_arm64 = machine in {"arm64", "aarch64"} + + nvidia_smi = shutil.which("nvidia-smi") + driver_cuda_version = None + compute_caps: list[str] = [] + visible_cuda_devices = os.environ.get("CUDA_VISIBLE_DEVICES") + visible_device_tokens = parse_cuda_visible_devices(visible_cuda_devices) + has_physical_nvidia = False + has_usable_nvidia = False + if nvidia_smi: + try: + result = run_capture([nvidia_smi], timeout = 20) + merged = "\n".join(part for part in (result.stdout, result.stderr) if part) + if "NVIDIA-SMI" in merged: + has_physical_nvidia = True + has_usable_nvidia = visible_device_tokens != [] + for line in merged.splitlines(): + if "CUDA Version:" in line: + raw = line.split("CUDA Version:", 1)[1].strip().split()[0] + major, minor = raw.split(".", 1) + driver_cuda_version = (int(major), int(minor)) + break + except Exception: + pass + + try: + caps = run_capture( + [ + nvidia_smi, + "--query-gpu=index,uuid,compute_cap", + "--format=csv,noheader", + ], + timeout = 20, + ) + visible_gpu_rows: list[tuple[str, str, str]] = [] + for raw in caps.stdout.splitlines(): + parts = [part.strip() for part in raw.split(",")] + if len(parts) != 3: + continue + index, uuid, cap = parts + visible_gpu_row = select_visible_gpu_rows( + [(index, uuid, cap)], + visible_device_tokens, + ) + if not visible_gpu_row: + continue + visible_gpu_rows.extend(visible_gpu_row) + normalized_cap = normalize_compute_cap(cap) + if normalized_cap is None: + continue + if normalized_cap not in compute_caps: + compute_caps.append(normalized_cap) + + if visible_gpu_rows: + has_usable_nvidia = True + elif visible_device_tokens == []: + has_usable_nvidia = False + elif supports_explicit_visible_device_matching(visible_device_tokens): + has_usable_nvidia = False + elif has_physical_nvidia: + has_usable_nvidia = True + except Exception: + pass + + return HostInfo( + system = system, + machine = machine, + is_windows = is_windows, + is_linux = is_linux, + is_macos = is_macos, + is_x86_64 = is_x86_64, + is_arm64 = is_arm64, + nvidia_smi = nvidia_smi, + driver_cuda_version = driver_cuda_version, + compute_caps = compute_caps, + visible_cuda_devices = visible_cuda_devices, + has_physical_nvidia = has_physical_nvidia, + has_usable_nvidia = has_usable_nvidia, + ) + + +def pick_windows_cuda_runtime(host: HostInfo) -> str | None: + if not host.driver_cuda_version: + return None + major, minor = host.driver_cuda_version + if major > 13 or (major == 13 and minor >= 1): + return "13.1" + if major > 12 or (major == 12 and minor >= 4): + return "12.4" + return None + + +def compatible_linux_runtime_lines(host: HostInfo) -> list[str]: + if not host.driver_cuda_version: + return [] + major, _minor = host.driver_cuda_version + if major >= 13: + return ["cuda13", "cuda12"] + if major >= 12: + return ["cuda12"] + return [] + + +def windows_runtime_line_info() -> dict[str, tuple[str, ...]]: + return { + "cuda13": ("cudart64_13*.dll", "cublas64_13*.dll", "cublasLt64_13*.dll"), + "cuda12": ("cudart64_12*.dll", "cublas64_12*.dll", "cublasLt64_12*.dll"), + } + + +def detected_windows_runtime_lines() -> tuple[list[str], dict[str, list[str]]]: + dirs = windows_runtime_dirs() + detected: list[str] = [] + runtime_dirs: dict[str, list[str]] = {} + for runtime_line, required_patterns in windows_runtime_line_info().items(): + matching_dirs = windows_runtime_dirs_for_patterns(required_patterns, dirs) + if matching_dirs: + detected.append(runtime_line) + runtime_dirs[runtime_line] = matching_dirs + return detected, runtime_dirs + + +def compatible_windows_runtime_lines(host: HostInfo) -> list[str]: + driver_runtime = pick_windows_cuda_runtime(host) + if driver_runtime == "13.1": + return ["cuda13", "cuda12"] + if driver_runtime == "12.4": + return ["cuda12"] + return [] + + +def runtime_line_from_cuda_version(cuda_version: str | None) -> str | None: + if not cuda_version: + return None + raw = str(cuda_version).strip() + if not raw: + return None + major, _, _ = raw.partition(".") + if major == "12": + return "cuda12" + if major == "13": + return "cuda13" + return None + + +def detect_torch_cuda_runtime_preference(host: HostInfo) -> CudaRuntimePreference: + selection_log: list[str] = [] + if host.is_macos: + selection_log.append("torch_cuda_preference: skipped on macOS") + return CudaRuntimePreference(runtime_line = None, selection_log = selection_log) + if not (host.has_usable_nvidia and (host.is_linux or host.is_windows)): + selection_log.append( + "torch_cuda_preference: skipped because CUDA host prerequisites were not met" + ) + return CudaRuntimePreference(runtime_line = None, selection_log = selection_log) + + try: + import torch + except Exception as exc: + selection_log.append(f"torch_cuda_preference: import failed: {exc}") + return CudaRuntimePreference(runtime_line = None, selection_log = selection_log) + + cuda_version = getattr(getattr(torch, "version", None), "cuda", None) + if not isinstance(cuda_version, str) or not cuda_version.strip(): + selection_log.append( + "torch_cuda_preference: torch.version.cuda missing; skipping Torch shortcut" + ) + return CudaRuntimePreference(runtime_line = None, selection_log = selection_log) + + try: + cuda_available = bool(torch.cuda.is_available()) + except Exception as exc: + selection_log.append( + f"torch_cuda_preference: torch.cuda.is_available() failed: {exc}" + ) + return CudaRuntimePreference(runtime_line = None, selection_log = selection_log) + + if not cuda_available: + selection_log.append( + "torch_cuda_preference: torch.cuda.is_available() returned False; falling back to normal selection" + ) + return CudaRuntimePreference(runtime_line = None, selection_log = selection_log) + + runtime_line = runtime_line_from_cuda_version(cuda_version) + if runtime_line is None: + selection_log.append( + f"torch_cuda_preference: unsupported torch.version.cuda={cuda_version}; falling back to normal selection" + ) + return CudaRuntimePreference(runtime_line = None, selection_log = selection_log) + + selection_log.append( + "torch_cuda_preference: selected runtime_line=" + f"{runtime_line} from torch.version.cuda={cuda_version}" + ) + return CudaRuntimePreference(runtime_line = runtime_line, selection_log = selection_log) + + +def windows_cuda_attempts( + host: HostInfo, + llama_tag: str, + upstream_assets: dict[str, str], + preferred_runtime_line: str | None, + selection_preamble: Iterable[str] = (), +) -> list[AssetChoice]: + selection_log = list(selection_preamble) + runtime_by_line = {"cuda12": "12.4", "cuda13": "13.1"} + driver_runtime = pick_windows_cuda_runtime(host) + detected_runtime_lines, runtime_dirs = detected_windows_runtime_lines() + compatible_runtime_lines = compatible_windows_runtime_lines(host) + normal_runtime_lines: list[str] + if detected_runtime_lines: + normal_runtime_lines = [ + line for line in compatible_runtime_lines if line in detected_runtime_lines + ] + else: + normal_runtime_lines = compatible_runtime_lines + selection_log.append( + "windows_cuda_selection: driver_runtime=" + + (driver_runtime if driver_runtime else "unknown") + ) + selection_log.append( + "windows_cuda_selection: detected_runtime_lines=" + + (",".join(detected_runtime_lines) if detected_runtime_lines else "none") + ) + for runtime_line in ("cuda13", "cuda12"): + selection_log.append( + "windows_cuda_selection: runtime_dirs " + f"{runtime_line}=" + + ( + ",".join(runtime_dirs.get(runtime_line, [])) + if runtime_dirs.get(runtime_line) + else "none" + ) + ) + if detected_runtime_lines: + selection_log.append( + "windows_cuda_selection: host_runtime_order=" + + (",".join(normal_runtime_lines) if normal_runtime_lines else "none") + ) + else: + selection_log.append( + "windows_cuda_selection: no CUDA runtime DLL line detected; falling back to driver order" + ) + if not normal_runtime_lines: + if detected_runtime_lines: + selection_log.append( + "windows_cuda_selection: detected CUDA runtime DLLs were incompatible with the reported driver" + ) + fallback_runtime_lines = ( + ["cuda13", "cuda12"] + if driver_runtime == "13.1" + else (["cuda12"] if driver_runtime == "12.4" else []) + ) + normal_runtime_lines = fallback_runtime_lines + + runtime_order: list[str] = [] + if preferred_runtime_line and preferred_runtime_line in normal_runtime_lines: + runtime_order.append(preferred_runtime_line) + selection_log.append( + "windows_cuda_selection: torch_preferred_runtime_line=" + f"{preferred_runtime_line} reordered_attempts" + ) + elif preferred_runtime_line: + selection_log.append( + "windows_cuda_selection: torch_preferred_runtime_line=" + f"{preferred_runtime_line} unavailable_or_incompatible" + ) + else: + selection_log.append( + "windows_cuda_selection: no Torch runtime preference available" + ) + + runtime_order.extend( + runtime_line + for runtime_line in normal_runtime_lines + if runtime_line not in runtime_order + ) + selection_log.append( + "windows_cuda_selection: normal_runtime_order=" + + (",".join(normal_runtime_lines) if normal_runtime_lines else "none") + ) + selection_log.append( + "windows_cuda_selection: attempt_runtime_order=" + + (",".join(runtime_order) if runtime_order else "none") + ) + + attempts: list[AssetChoice] = [] + for runtime_line in runtime_order: + runtime = runtime_by_line[runtime_line] + upstream_name = f"llama-{llama_tag}-bin-win-cuda-{runtime}-x64.zip" + asset_url = upstream_assets.get(upstream_name) + if not asset_url: + selection_log.append( + f"windows_cuda_selection: skip missing asset {upstream_name}" + ) + continue + attempts.append( + AssetChoice( + repo = UPSTREAM_REPO, + tag = llama_tag, + name = upstream_name, + url = asset_url, + source_label = "upstream", + install_kind = "windows-cuda", + runtime_line = runtime_line, + selection_log = list(selection_log) + + [ + f"windows_cuda_selection: selected {upstream_name} runtime={runtime}" + ], + ) + ) + return attempts + + +def resolve_windows_cuda_choices( + host: HostInfo, llama_tag: str, upstream_assets: dict[str, str] +) -> list[AssetChoice]: + torch_preference = detect_torch_cuda_runtime_preference(host) + attempts = windows_cuda_attempts( + host, + llama_tag, + upstream_assets, + torch_preference.runtime_line, + torch_preference.selection_log, + ) + return attempts + + +def resolve_linux_cuda_choice( + host: HostInfo, llama_tag: str, published_repo: str, published_release_tag: str +) -> LinuxCudaSelection: + torch_preference = detect_torch_cuda_runtime_preference(host) + skipped_tag_mismatches = 0 + for release in iter_published_release_bundles( + published_repo, published_release_tag + ): + if release.upstream_tag != llama_tag: + skipped_tag_mismatches += 1 + continue + selection = linux_cuda_choice_from_release( + host, + release, + preferred_runtime_line = torch_preference.runtime_line, + selection_preamble = torch_preference.selection_log, + ) + if selection is not None: + return selection + if skipped_tag_mismatches: + log( + "published Linux CUDA selection skipped " + f"{skipped_tag_mismatches} release(s) with upstream_tag != {llama_tag}" + ) + raise PrebuiltFallback("no compatible published Linux CUDA bundle was found") + + +def resolve_upstream_asset_choice(host: HostInfo, llama_tag: str) -> AssetChoice: + upstream_assets = github_release_assets(UPSTREAM_REPO, llama_tag) + if host.is_linux and host.is_x86_64: + upstream_name = f"llama-{llama_tag}-bin-ubuntu-x64.tar.gz" + if upstream_name not in upstream_assets: + raise PrebuiltFallback("upstream Linux CPU asset was not found") + return AssetChoice( + repo = UPSTREAM_REPO, + tag = llama_tag, + name = upstream_name, + url = upstream_assets[upstream_name], + source_label = "upstream", + install_kind = "linux-cpu", + ) + + if host.is_windows and host.is_x86_64: + if host.has_usable_nvidia: + attempts = resolve_windows_cuda_choices(host, llama_tag, upstream_assets) + if attempts: + return attempts[0] + raise PrebuiltFallback("no compatible Windows CUDA asset was found") + + upstream_name = f"llama-{llama_tag}-bin-win-cpu-x64.zip" + if upstream_name not in upstream_assets: + raise PrebuiltFallback("upstream Windows CPU asset was not found") + return AssetChoice( + repo = UPSTREAM_REPO, + tag = llama_tag, + name = upstream_name, + url = upstream_assets[upstream_name], + source_label = "upstream", + install_kind = "windows-cpu", + ) + + if host.is_macos and host.is_arm64: + upstream_name = f"llama-{llama_tag}-bin-macos-arm64.tar.gz" + if upstream_name not in upstream_assets: + raise PrebuiltFallback("upstream macOS arm64 asset was not found") + return AssetChoice( + repo = UPSTREAM_REPO, + tag = llama_tag, + name = upstream_name, + url = upstream_assets[upstream_name], + source_label = "upstream", + install_kind = "macos-arm64", + ) + + if host.is_macos and host.is_x86_64: + upstream_name = f"llama-{llama_tag}-bin-macos-x64.tar.gz" + if upstream_name not in upstream_assets: + raise PrebuiltFallback("upstream macOS x64 asset was not found") + return AssetChoice( + repo = UPSTREAM_REPO, + tag = llama_tag, + name = upstream_name, + url = upstream_assets[upstream_name], + source_label = "upstream", + install_kind = "macos-x64", + ) + + raise PrebuiltFallback( + f"no prebuilt policy exists for {host.system} {host.machine}" + ) + + +def resolve_asset_choice( + host: HostInfo, llama_tag: str, published_repo: str, published_release_tag: str +) -> AssetChoice: + if host.is_linux and host.is_x86_64 and host.has_usable_nvidia: + return resolve_linux_cuda_choice( + host, llama_tag, published_repo, published_release_tag + ).primary + return resolve_upstream_asset_choice(host, llama_tag) + + +def extract_archive(archive_path: Path, destination: Path) -> None: + def safe_extract_path(base: Path, member_name: str) -> Path: + normalized = member_name.replace("\\", "/") + member_path = Path(normalized) + if member_path.is_absolute(): + raise PrebuiltFallback( + f"archive member used an absolute path: {member_name}" + ) + + target = (base / member_path).resolve() + base_resolved = base.resolve() + try: + target.relative_to(base_resolved) + except ValueError as exc: + raise PrebuiltFallback( + f"archive member escaped destination: {member_name}" + ) from exc + return target + + def safe_link_target( + base: Path, member_name: str, link_name: str, target: Path + ) -> tuple[str, Path]: + normalized = link_name.replace("\\", "/") + link_path = Path(normalized) + if link_path.is_absolute(): + raise PrebuiltFallback( + f"archive link used an absolute target: {member_name} -> {link_name}" + ) + if not normalized: + raise PrebuiltFallback(f"archive link used an empty target: {member_name}") + + resolved = (target.parent / link_path).resolve() + base_resolved = base.resolve() + try: + resolved.relative_to(base_resolved) + except ValueError as exc: + raise PrebuiltFallback( + f"archive link escaped destination: {member_name} -> {link_name}" + ) from exc + return normalized, resolved + + def extract_zip_safely(source: Path, base: Path) -> None: + with zipfile.ZipFile(source) as archive: + for member in archive.infolist(): + target = safe_extract_path(base, member.filename) + mode = (member.external_attr >> 16) & 0o170000 + if mode == 0o120000: + raise PrebuiltFallback( + f"zip archive contained a symlink entry: {member.filename}" + ) + if member.is_dir(): + target.mkdir(parents = True, exist_ok = True) + continue + target.parent.mkdir(parents = True, exist_ok = True) + with archive.open(member, "r") as src, target.open("wb") as dst: + shutil.copyfileobj(src, dst) + + def extract_tar_safely(source: Path, base: Path) -> None: + pending_links: list[tuple[tarfile.TarInfo, Path]] = [] + with tarfile.open(source, "r:gz") as archive: + for member in archive.getmembers(): + target = safe_extract_path(base, member.name) + if member.isdir(): + target.mkdir(parents = True, exist_ok = True) + continue + if member.islnk() or member.issym(): + pending_links.append((member, target)) + continue + if not member.isfile(): + raise PrebuiltFallback( + f"tar archive contained an unsupported entry: {member.name}" + ) + target.parent.mkdir(parents = True, exist_ok = True) + extracted = archive.extractfile(member) + if extracted is None: + raise PrebuiltFallback( + f"tar archive entry could not be read: {member.name}" + ) + with extracted, target.open("wb") as dst: + shutil.copyfileobj(extracted, dst) + + unresolved = list(pending_links) + while unresolved: + next_round: list[tuple[tarfile.TarInfo, Path]] = [] + progressed = False + for member, target in unresolved: + normalized_link, resolved_target = safe_link_target( + base, member.name, member.linkname, target + ) + if not resolved_target.exists() and not resolved_target.is_symlink(): + next_round.append((member, target)) + continue + if resolved_target.is_dir(): + raise PrebuiltFallback( + f"archive link targeted a directory: {member.name} -> {member.linkname}" + ) + + target.parent.mkdir(parents = True, exist_ok = True) + if target.exists() or target.is_symlink(): + target.unlink() + + if member.issym(): + target.symlink_to(normalized_link) + else: + shutil.copy2(resolved_target, target) + progressed = True + + if not progressed: + details = ", ".join( + f"{member.name} -> {member.linkname}" for member, _ in next_round + ) + raise PrebuiltFallback( + f"tar archive contained unresolved link entries: {details}" + ) + unresolved = next_round + + destination.mkdir(parents = True, exist_ok = True) + if archive_path.name.endswith(".zip"): + extract_zip_safely(archive_path, destination) + return + if archive_path.name.endswith(".tar.gz"): + extract_tar_safely(archive_path, destination) + return + raise PrebuiltFallback(f"unsupported archive format: {archive_path.name}") + + +def copy_globs( + source_dir: Path, destination: Path, patterns: list[str], *, required: bool = True +) -> None: + destination.mkdir(parents = True, exist_ok = True) + matched_sources: dict[str, Path] = {} + for path in sorted( + (candidate for candidate in source_dir.rglob("*") if candidate.is_file()), + key = lambda candidate: ( + len(candidate.relative_to(source_dir).parts), + str(candidate), + ), + ): + for pattern in patterns: + if fnmatch.fnmatch(path.name, pattern): + previous = matched_sources.get(path.name) + if previous is not None and previous != path: + raise PrebuiltFallback( + f"ambiguous archive layout for {path.name}: " + f"{previous.relative_to(source_dir)} and {path.relative_to(source_dir)}" + ) + matched_sources[path.name] = path + break + + if required and not matched_sources: + raise PrebuiltFallback(f"required files missing from {source_dir}: {patterns}") + + for name, path in matched_sources.items(): + shutil.copy2(path, destination / name) + + +def ensure_converter_scripts(install_dir: Path, llama_tag: str) -> None: + canonical = install_dir / "convert_hf_to_gguf.py" + if not canonical.exists(): + # Hydrated source tree should have placed this file already. + # Fall back to a network fetch so the install is not blocked. + raw_base = f"https://raw.githubusercontent.com/ggml-org/llama.cpp/{llama_tag}" + source_url = f"{raw_base}/convert_hf_to_gguf.py" + data = download_bytes( + source_url, + progress_label = f"Downloading {download_label_from_url(source_url)}", + ) + if not data: + raise RuntimeError(f"downloaded empty converter script from {source_url}") + if b"import " not in data and b"def " not in data and b"#!/" not in data: + raise RuntimeError( + f"downloaded converter script did not look like Python source: {source_url}" + ) + atomic_write_bytes(canonical, data) + legacy = install_dir / "convert-hf-to-gguf.py" + if legacy.exists() or legacy.is_symlink(): + legacy.unlink() + try: + legacy.symlink_to("convert_hf_to_gguf.py") + except OSError: + shutil.copy2(canonical, legacy) + + +def extracted_archive_root(extract_dir: Path) -> Path: + children = [path for path in extract_dir.iterdir()] + if len(children) == 1 and children[0].is_dir(): + return children[0] + return extract_dir + + +def copy_directory_contents(source_dir: Path, destination: Path) -> None: + destination.mkdir(parents = True, exist_ok = True) + for item in source_dir.iterdir(): + target = destination / item.name + if item.is_dir(): + shutil.copytree(item, target, dirs_exist_ok = True) + else: + shutil.copy2(item, target) + + +def hydrate_source_tree( + upstream_tag: str, + install_dir: Path, + work_dir: Path, + *, + expected_sha256: str, +) -> None: + archive_path = work_dir / f"llama.cpp-source-{upstream_tag}.tar.gz" + source_urls = upstream_source_archive_urls(upstream_tag) + extract_dir = Path(tempfile.mkdtemp(prefix = "source-extract-", dir = work_dir)) + + try: + log(f"downloading llama.cpp source tree for upstream tag {upstream_tag}") + last_exc: Exception | None = None + downloaded = False + for index, source_url in enumerate(source_urls): + try: + if index > 0: + log( + f"retrying source tree download from fallback URL: {source_url}" + ) + download_file_verified( + source_url, + archive_path, + expected_sha256 = expected_sha256, + label = f"llama.cpp source tree for {upstream_tag}", + ) + downloaded = True + break + except Exception as exc: + last_exc = exc + if index == len(source_urls) - 1: + raise + log(f"source tree download failed from {source_url}: {exc}") + if not downloaded: + assert last_exc is not None + raise last_exc + extract_archive(archive_path, extract_dir) + source_root = extracted_archive_root(extract_dir) + required_paths = [ + source_root / "CMakeLists.txt", + source_root / "convert_hf_to_gguf.py", + source_root / "gguf-py", + ] + missing = [ + str(path.relative_to(source_root)) + for path in required_paths + if not path.exists() + ] + if missing: + raise PrebuiltFallback( + "upstream source archive was missing required repo files: " + + ", ".join(missing) + ) + copy_directory_contents(source_root, install_dir) + except PrebuiltFallback: + raise + except Exception as exc: + raise PrebuiltFallback( + f"failed to hydrate upstream llama.cpp source tree for {upstream_tag}: {exc}" + ) from exc + finally: + remove_tree(extract_dir) + + +def normalize_install_layout(install_dir: Path, host: HostInfo) -> tuple[Path, Path]: + build_bin = install_dir / "build" / "bin" + if host.is_windows: + exec_dir = build_bin / "Release" + exec_dir.mkdir(parents = True, exist_ok = True) + return exec_dir / "llama-server.exe", exec_dir / "llama-quantize.exe" + + install_dir.mkdir(parents = True, exist_ok = True) + build_bin.mkdir(parents = True, exist_ok = True) + return install_dir / "llama-server", install_dir / "llama-quantize" + + +def discover_installed_executable(install_dir: Path, executable_name: str) -> Path: + direct = install_dir / executable_name + if direct.exists() and direct.is_file(): + return direct + candidate = next( + (path for path in install_dir.rglob(executable_name) if path.is_file()), None + ) + if candidate is None: + raise PrebuiltFallback(f"{executable_name} was not installed") + return candidate + + +def write_exec_wrapper(entrypoint: Path, target: Path) -> None: + relative_target = os.path.relpath(target, entrypoint.parent) + script = "\n".join( + [ + "#!/bin/sh", + f'exec "$(dirname "$0")/{relative_target}" "$@"', + "", + ] + ) + atomic_write_bytes(entrypoint, script.encode("utf-8")) + os.chmod(entrypoint, 0o755) + + +def create_exec_entrypoint(entrypoint: Path, target: Path) -> None: + if entrypoint == target: + return + if entrypoint.exists() or entrypoint.is_symlink(): + entrypoint.unlink() + try: + entrypoint.symlink_to(os.path.relpath(target, entrypoint.parent)) + except Exception: + write_exec_wrapper(entrypoint, target) + + +def overlay_directory_for_choice( + install_dir: Path, choice: AssetChoice, host: HostInfo +) -> Path: + if host.is_windows or choice.install_kind.startswith("windows"): + path = install_dir / "build" / "bin" / "Release" + else: + path = install_dir / "build" / "bin" + path.mkdir(parents = True, exist_ok = True) + return path + + +def runtime_patterns_for_choice(choice: AssetChoice) -> list[str]: + if choice.install_kind in {"linux-cpu", "linux-cuda"}: + return [ + "llama-server", + "llama-quantize", + "libllama.so*", + "libggml.so*", + "libggml-base.so*", + "libmtmd.so*", + "libggml-cpu-*.so*", + "libggml-cuda.so*", + "libggml-rpc.so*", + ] + if choice.install_kind in {"macos-arm64", "macos-x64"}: + return ["llama-server", "llama-quantize", "lib*.dylib"] + if choice.install_kind in {"windows-cpu", "windows-cuda"}: + return ["*.exe", "*.dll"] + raise PrebuiltFallback( + f"unsupported install kind for runtime overlay: {choice.install_kind}" + ) + + +def metadata_patterns_for_choice(choice: AssetChoice) -> list[str]: + patterns = ["BUILD_INFO.txt", "THIRD_PARTY_LICENSES.txt"] + if choice.install_kind.startswith("windows"): + patterns.append("LICENSE.txt") + else: + patterns.append("LICENSE") + return patterns + + +@contextmanager +def install_lock(lock_path: Path) -> Iterator[None]: + lock_path.parent.mkdir(parents = True, exist_ok = True) + + if FileLock is None: + # Fallback: exclusive file creation as a simple lock. + # Write our PID so stale locks from crashed processes can be detected. + fd: int | None = None + deadline = time.monotonic() + INSTALL_LOCK_TIMEOUT_SECONDS + while True: + try: + fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_RDWR) + os.write(fd, f"{os.getpid()}\n".encode()) + os.fsync(fd) + break + except FileExistsError: + # Check if the holder process is still alive + stale = False + try: + raw = lock_path.read_text().strip() + except FileNotFoundError: + # Lock vanished between our open attempt and read -- retry + continue + if not raw: + # File exists but PID not yet written -- another process + # just created it. Wait briefly for the write to land. + time.sleep(0.1) + continue + try: + holder_pid = int(raw) + os.kill(holder_pid, 0) # signal 0 = existence check + except ValueError: + # PID unreadable (corrupted file) + stale = True + except ProcessLookupError: + # Process is dead + stale = True + except PermissionError: + # Process is alive but owned by another user -- not stale + pass + if stale: + lock_path.unlink(missing_ok = True) + continue + if time.monotonic() >= deadline: + raise RuntimeError( + f"timed out after {INSTALL_LOCK_TIMEOUT_SECONDS}s waiting for concurrent install lock: {lock_path}" + ) + time.sleep(0.5) + try: + yield + finally: + if fd is not None: + os.close(fd) + lock_path.unlink(missing_ok = True) + return + + try: + with FileLock(lock_path, timeout = INSTALL_LOCK_TIMEOUT_SECONDS): + yield + except FileLockTimeout as exc: + raise RuntimeError( + f"timed out after {INSTALL_LOCK_TIMEOUT_SECONDS}s waiting for concurrent install lock: {lock_path}" + ) from exc + + +def install_lock_path(install_dir: Path) -> Path: + return install_dir.parent / f".{install_dir.name}.install.lock" + + +def install_staging_root(install_dir: Path) -> Path: + root = install_dir.parent / INSTALL_STAGING_ROOT_NAME + root.mkdir(parents = True, exist_ok = True) + return root + + +def prune_install_staging_root(install_dir: Path) -> None: + root = install_dir.parent / INSTALL_STAGING_ROOT_NAME + try: + root.rmdir() + except OSError: + pass + + +def create_install_staging_dir(install_dir: Path) -> Path: + staging_dir = Path( + tempfile.mkdtemp( + prefix = f"{install_dir.name}.staging-", dir = install_staging_root(install_dir) + ) + ) + log(f"created install staging dir {staging_dir}") + return staging_dir + + +def unique_install_side_path(install_dir: Path, label: str) -> Path: + root = install_staging_root(install_dir) + timestamp = time.strftime("%Y%m%d%H%M%S", time.gmtime()) + prefix = f"{install_dir.name}.{label}-{timestamp}-{os.getpid()}" + candidate = root / prefix + counter = 0 + while candidate.exists(): + counter += 1 + candidate = root / f"{prefix}-{counter}" + return candidate + + +def remove_tree(path: Path | None) -> None: + if path and path.exists(): + shutil.rmtree(path, ignore_errors = True) + + +def remove_tree_logged(path: Path | None, label: str) -> None: + if not path: + return + if not path.exists(): + log(f"{label} already absent at {path}") + return + log(f"removing {label} at {path}") + try: + shutil.rmtree(path) + except Exception as exc: + log(f"failed to remove {label} at {path}: {exc}") + raise + + +def cleanup_install_side_paths( + install_dir: Path, + *, + staging_dir: Path | None = None, + rollback_dir: Path | None = None, + failed_dir: Path | None = None, + active_dir: Path | None = None, +) -> None: + cleanup_failures: list[str] = [] + for label, path in ( + ("failed install path", failed_dir), + ("rollback path", rollback_dir), + ("active install path", active_dir), + ("staging dir", staging_dir), + ): + if not path: + continue + try: + remove_tree_logged(path, label) + except Exception as exc: + cleanup_failures.append(f"{label} ({path}): {exc}") + prune_install_staging_root(install_dir) + if cleanup_failures: + raise RuntimeError("cleanup failed for " + "; ".join(cleanup_failures)) + + +def confirm_install_tree(install_dir: Path, host: HostInfo) -> None: + if host.is_windows: + expected = [ + install_dir / "build" / "bin" / "Release" / "llama-server.exe", + install_dir / "build" / "bin" / "Release" / "llama-quantize.exe", + install_dir / "convert_hf_to_gguf.py", + install_dir / "gguf-py", + ] + else: + expected = [ + install_dir / "llama-server", + install_dir / "llama-quantize", + install_dir / "build" / "bin" / "llama-server", + install_dir / "build" / "bin" / "llama-quantize", + install_dir / "convert_hf_to_gguf.py", + install_dir / "gguf-py", + ] + + expected.append(install_dir / "UNSLOTH_PREBUILT_INFO.json") + missing = [str(path) for path in expected if not path.exists()] + if missing: + raise RuntimeError( + "activated install was missing expected files: " + ", ".join(missing) + ) + + +def activate_install_tree(staging_dir: Path, install_dir: Path, host: HostInfo) -> None: + rollback_dir: Path | None = None + failed_dir: Path | None = None + try: + if install_dir.exists(): + rollback_dir = unique_install_side_path(install_dir, "rollback") + log(f"moving existing install to rollback path {rollback_dir}") + os.replace(install_dir, rollback_dir) + log(f"moved existing install to rollback path {rollback_dir.name}") + + log(f"activating staged install {staging_dir} -> {install_dir}") + os.replace(staging_dir, install_dir) + log(f"activated staged install at {install_dir}") + log(f"confirming activated install tree at {install_dir}") + confirm_install_tree(install_dir, host) + log(f"activated install tree confirmed at {install_dir}") + except Exception as exc: + log(f"activation failed for staged install: {exc}") + try: + if install_dir.exists(): + failed_dir = unique_install_side_path(install_dir, "failed") + log(f"moving failed active install to {failed_dir}") + os.replace(install_dir, failed_dir) + elif staging_dir.exists(): + failed_dir = staging_dir + staging_dir = None + log(f"retaining failed staging tree at {failed_dir}") + + if rollback_dir and rollback_dir.exists(): + log(f"restoring rollback path {rollback_dir} -> {install_dir}") + os.replace(rollback_dir, install_dir) + log(f"restored previous install from rollback path {rollback_dir.name}") + raise PrebuiltFallback( + "staged prebuilt validation passed but activation failed; restored previous install " + f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})" + ) from exc + except PrebuiltFallback: + raise + except Exception as rollback_exc: + log(f"rollback after failed activation also failed: {rollback_exc}") + + log( + "rollback restoration failed; cleaning staging, install, and rollback paths before source build fallback" + ) + cleanup_error: Exception | None = None + try: + cleanup_install_side_paths( + install_dir, + staging_dir = staging_dir, + rollback_dir = rollback_dir, + failed_dir = failed_dir, + active_dir = install_dir, + ) + except Exception as cleanup_exc: + cleanup_error = cleanup_exc + log(f"cleanup after rollback failure also failed: {cleanup_exc}") + details = textwrap.shorten(str(exc), width = 200, placeholder = "...") + if cleanup_error is not None: + raise PrebuiltFallback( + "staged prebuilt validation passed but activation and rollback failed; " + f"cleanup also reported errors ({details}; cleanup={cleanup_error})" + ) from exc + raise PrebuiltFallback( + "staged prebuilt validation passed but activation and rollback failed; " + f"cleaned install state for fresh source build ({details})" + ) from exc + else: + if rollback_dir: + remove_tree_logged(rollback_dir, "rollback path") + finally: + remove_tree(failed_dir) + remove_tree(staging_dir) + prune_install_staging_root(install_dir) + + +def install_from_archives( + choice: AssetChoice, host: HostInfo, install_dir: Path, work_dir: Path +) -> tuple[Path, Path]: + main_archive = work_dir / choice.name + log(f"downloading {choice.name} from {choice.source_label} release") + if not choice.expected_sha256: + raise PrebuiltFallback( + f"approved checksum was missing for selected asset {choice.name}" + ) + download_file_verified( + choice.url, + main_archive, + expected_sha256 = choice.expected_sha256, + label = f"prebuilt archive {choice.name}", + ) + + install_dir.mkdir(parents = True, exist_ok = True) + extract_dir = Path(tempfile.mkdtemp(prefix = "extract-", dir = work_dir)) + + try: + extract_archive(main_archive, extract_dir) + source_dir = extract_dir + overlay_dir = overlay_directory_for_choice(install_dir, choice, host) + copy_globs( + source_dir, overlay_dir, runtime_patterns_for_choice(choice), required = True + ) + copy_globs( + source_dir, + install_dir, + metadata_patterns_for_choice(choice), + required = False, + ) + finally: + remove_tree(extract_dir) + + if host.is_windows: + exec_dir = install_dir / "build" / "bin" / "Release" + server_src = next(exec_dir.glob("llama-server.exe"), None) + quantize_src = next(exec_dir.glob("llama-quantize.exe"), None) + if server_src is None or quantize_src is None: + raise PrebuiltFallback("windows executables were not installed correctly") + return server_src, quantize_src + + build_bin = install_dir / "build" / "bin" + source_server = build_bin / "llama-server" + source_quantize = build_bin / "llama-quantize" + if not source_server.exists() or not source_quantize.exists(): + raise PrebuiltFallback( + "unix executables were not installed correctly into build/bin" + ) + os.chmod(source_server, 0o755) + os.chmod(source_quantize, 0o755) + + root_server = install_dir / "llama-server" + root_quantize = install_dir / "llama-quantize" + if source_server != root_server: + create_exec_entrypoint(root_server, source_server) + if source_quantize != root_quantize: + create_exec_entrypoint(root_quantize, source_quantize) + build_server = build_bin / "llama-server" + build_quantize = build_bin / "llama-quantize" + if source_server != build_server: + create_exec_entrypoint(build_server, source_server) + if source_quantize != build_quantize: + create_exec_entrypoint(build_quantize, source_quantize) + + return source_server, source_quantize + + +def ensure_repo_shape(install_dir: Path) -> None: + required = [ + install_dir / "CMakeLists.txt", + install_dir / "convert_hf_to_gguf.py", + install_dir / "gguf-py", + ] + missing = [ + str(path.relative_to(install_dir)) for path in required if not path.exists() + ] + if missing: + raise PrebuiltFallback( + "hydrated llama.cpp source tree was missing: " + ", ".join(missing) + ) + + +def validation_model_cache_path(install_dir: Path) -> Path: + cache_dir = install_dir.parent / VALIDATION_MODEL_CACHE_DIRNAME + cache_dir.mkdir(parents = True, exist_ok = True) + return cache_dir / VALIDATION_MODEL_CACHE_FILENAME + + +def validated_validation_model_bytes(data: bytes) -> bytes: + if not data: + raise RuntimeError(f"downloaded empty validation model from {TEST_MODEL_URL}") + digest = hashlib.sha256(data).hexdigest() + if digest != TEST_MODEL_SHA256: + raise RuntimeError( + "validation model checksum mismatch: " + f"expected={TEST_MODEL_SHA256} actual={digest}" + ) + return data + + +def download_validation_model(path: Path, cache_path: Path | None = None) -> None: + try: + data: bytes | None = None + if cache_path and cache_path.exists(): + try: + data = validated_validation_model_bytes(cache_path.read_bytes()) + log(f"using cached tiny GGUF validation model from {cache_path}") + except Exception as exc: + log( + f"cached tiny GGUF validation model was invalid; refreshing cache ({exc})" + ) + data = None + if data is None: + log("downloading tiny GGUF validation model") + data = validated_validation_model_bytes( + download_bytes( + TEST_MODEL_URL, + progress_label = f"Downloading {download_label_from_url(TEST_MODEL_URL)}", + ) + ) + if cache_path is not None: + atomic_write_bytes(cache_path, data) + atomic_write_bytes(path, data) + except Exception as exc: + raise PrebuiltFallback(f"validation model unavailable: {exc}") from exc + + +def free_local_port() -> int: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(("127.0.0.1", 0)) + _, port = sock.getsockname() + sock.close() + return int(port) + + +def read_log_excerpt(log_path: Path, *, max_lines: int = 60) -> str: + try: + content = log_path.read_text(encoding = "utf-8", errors = "replace") + except FileNotFoundError: + return "" + return "\n".join(content.splitlines()[-max_lines:]) + + +def is_retryable_server_bind_error( + exc: Exception | None, + output: str = "", + *, + exited_quickly: bool = False, +) -> bool: + haystack = output.lower() + bind_markers = ( + "address already in use", + "only one usage of each socket address", + "failed to bind", + "bind failed", + "failed to listen", + "errno 98", + "errno 10048", + ) + if any(marker in haystack for marker in bind_markers): + return True + + if isinstance(exc, urllib.error.URLError): + reason = exc.reason + if exited_quickly and isinstance(reason, ConnectionRefusedError): + return True + if isinstance(reason, OSError) and reason.errno in { + 98, + 99, + 111, + 10048, + 10049, + 10061, + }: + return exited_quickly + if exited_quickly and isinstance(exc, ConnectionRefusedError): + return True + if isinstance(exc, OSError) and exc.errno in {98, 99, 111, 10048, 10049, 10061}: + return exited_quickly + return False + + +def dedupe_existing_dirs(paths: Iterable[str | Path]) -> list[str]: + unique: list[str] = [] + seen: set[str] = set() + for raw in paths: + if not raw: + continue + path = Path(raw).expanduser() + if not path.is_dir(): + continue + resolved = str(path.resolve()) + if resolved in seen: + continue + seen.add(resolved) + unique.append(resolved) + return unique + + +def linux_missing_libraries( + binary_path: Path, *, env: dict[str, str] | None = None +) -> list[str]: + try: + result = run_capture(["ldd", str(binary_path)], timeout = 20, env = env) + except Exception: + return [] + + missing: list[str] = [] + for line in (result.stdout + result.stderr).splitlines(): + line = line.strip() + if "=> not found" not in line: + continue + library = line.split("=>", 1)[0].strip() + if library and library not in missing: + missing.append(library) + return missing + + +def python_runtime_dirs() -> list[str]: + candidates: list[Path] = [] + search_roots = [Path(entry) for entry in sys.path if entry] + try: + search_roots.extend(Path(path) for path in site.getsitepackages()) + except Exception: + pass + try: + user_site = site.getusersitepackages() + if user_site: + search_roots.append(Path(user_site)) + except Exception: + pass + + for root in search_roots: + if not root.is_dir(): + continue + candidates.extend(root.glob("nvidia/*/lib")) + candidates.extend(root.glob("nvidia/*/bin")) + candidates.extend(root.glob("torch/lib")) + return dedupe_existing_dirs(candidates) + + +def ldconfig_runtime_dirs(required_libraries: Iterable[str]) -> list[str]: + try: + result = run_capture(["ldconfig", "-p"], timeout = 20) + except Exception: + return [] + + required = set(required_libraries) + candidates: list[str] = [] + for line in result.stdout.splitlines(): + if "=>" not in line: + continue + library, _, location = line.partition("=>") + library = library.strip().split()[0] + if required and library not in required: + continue + path = Path(location.strip()).parent + candidates.append(str(path)) + return dedupe_existing_dirs(candidates) + + +def linux_runtime_dirs(binary_path: Path) -> list[str]: + missing = linux_missing_libraries(binary_path) + if not missing: + return [] + return linux_runtime_dirs_for_required_libraries(missing) + + +def preflight_linux_installed_binaries( + binaries: Iterable[Path], + install_dir: Path, + host: HostInfo, +) -> None: + if not host.is_linux: + return + + issues: list[str] = [] + for binary_path in binaries: + env = binary_env(binary_path, install_dir, host) + missing = linux_missing_libraries(binary_path, env = env) + if not missing: + continue + runtime_dirs = [ + part for part in env.get("LD_LIBRARY_PATH", "").split(os.pathsep) if part + ] + issues.append( + f"{binary_path.name}: missing={','.join(missing)} " + f"ld_library_path={','.join(runtime_dirs) if runtime_dirs else 'none'}" + ) + + if issues: + raise PrebuiltFallback( + "linux extracted binary preflight failed:\n" + "\n".join(issues) + ) + + +def glob_paths(*patterns: str) -> list[str]: + matches: list[str] = [] + for pattern in patterns: + if any(char in pattern for char in "*?[]"): + matches.extend(str(path) for path in Path("/").glob(pattern.lstrip("/"))) + else: + matches.append(pattern) + return matches + + +def windows_runtime_dirs() -> list[str]: + candidates: list[str | Path] = [] + + env_dirs = os.environ.get("CUDA_RUNTIME_DLL_DIR", "") + if env_dirs: + candidates.extend(part for part in env_dirs.split(os.pathsep) if part) + + path_dirs = os.environ.get("PATH", "") + if path_dirs: + candidates.extend(part for part in path_dirs.split(os.pathsep) if part) + + cuda_roots: list[Path] = [] + for name in ("CUDA_PATH", "CUDA_HOME", "CUDA_ROOT"): + value = os.environ.get(name) + if value: + cuda_roots.append(Path(value)) + + for root in cuda_roots: + candidates.extend([root / "bin", root / "lib" / "x64"]) + + program_files = os.environ.get("ProgramFiles", r"C:\Program Files") + toolkit_base = Path(program_files) / "NVIDIA GPU Computing Toolkit" / "CUDA" + if toolkit_base.is_dir(): + candidates.extend(toolkit_base.glob("v*/bin")) + candidates.extend(toolkit_base.glob("v*/lib/x64")) + + candidates.extend(Path(path) for path in python_runtime_dirs()) + return dedupe_existing_dirs(candidates) + + +def windows_runtime_dirs_for_patterns( + required_patterns: Iterable[str], + candidate_dirs: Iterable[str] | None = None, +) -> list[str]: + directories = ( + list(candidate_dirs) if candidate_dirs is not None else windows_runtime_dirs() + ) + matching_dirs: list[str] = [] + for pattern in required_patterns: + matched_dirs = [ + directory for directory in directories if any(Path(directory).glob(pattern)) + ] + if not matched_dirs: + return [] + for directory in matched_dirs: + if directory not in matching_dirs: + matching_dirs.append(directory) + return matching_dirs + + +def windows_runtime_dirs_for_runtime_line(runtime_line: str | None) -> list[str]: + if not runtime_line: + return [] + patterns = windows_runtime_line_info().get(runtime_line) + if not patterns: + return [] + return windows_runtime_dirs_for_patterns(patterns) + + +def binary_env( + binary_path: Path, + install_dir: Path, + host: HostInfo, + *, + runtime_line: str | None = None, +) -> dict[str, str]: + env = os.environ.copy() + if host.is_windows: + path_dirs = [ + str(binary_path.parent), + *windows_runtime_dirs_for_runtime_line(runtime_line), + ] + existing = [part for part in env.get("PATH", "").split(os.pathsep) if part] + env["PATH"] = os.pathsep.join(dedupe_existing_dirs([*path_dirs, *existing])) + elif host.is_linux: + ld_dirs = [ + str(binary_path.parent), + str(install_dir), + *linux_runtime_dirs(binary_path), + ] + existing = [ + part for part in env.get("LD_LIBRARY_PATH", "").split(os.pathsep) if part + ] + env["LD_LIBRARY_PATH"] = os.pathsep.join( + dedupe_existing_dirs([*ld_dirs, *existing]) + ) + elif host.is_macos: + dyld_dirs = [str(binary_path.parent), str(install_dir)] + existing = [ + part for part in env.get("DYLD_LIBRARY_PATH", "").split(os.pathsep) if part + ] + env["DYLD_LIBRARY_PATH"] = os.pathsep.join( + dedupe_existing_dirs([*dyld_dirs, *existing]) + ) + return env + + +def validate_quantize( + quantize_path: Path, + probe_path: Path, + quantized_path: Path, + install_dir: Path, + host: HostInfo, + *, + runtime_line: str | None = None, +) -> None: + command = [str(quantize_path), str(probe_path), str(quantized_path), "Q6_K", "2"] + result = subprocess.run( + command, + capture_output = True, + text = True, + timeout = 120, + env = binary_env(quantize_path, install_dir, host, runtime_line = runtime_line), + ) + if ( + result.returncode != 0 + or not quantized_path.exists() + or quantized_path.stat().st_size == 0 + ): + raise PrebuiltFallback( + "llama-quantize validation failed:\n" + + result.stdout + + ("\n" + result.stderr if result.stderr else "") + ) + + +def validate_server( + server_path: Path, + probe_path: Path, + host: HostInfo, + install_dir: Path, + *, + runtime_line: str | None = None, +) -> None: + last_failure: PrebuiltFallback | None = None + for port_attempt in range(1, SERVER_PORT_BIND_ATTEMPTS + 1): + port = free_local_port() + command = [ + str(server_path), + "-m", + str(probe_path), + "--host", + "127.0.0.1", + "--port", + str(port), + "-c", + "32", + "--parallel", + "1", + "--threads", + "1", + "--ubatch-size", + "32", + "--batch-size", + "32", + ] + if host.has_usable_nvidia or (host.is_macos and host.is_arm64): + command.extend(["--n-gpu-layers", "1"]) + + log_fd, log_name = tempfile.mkstemp(prefix = "llama-server-", suffix = ".log") + os.close(log_fd) + log_path = Path(log_name) + process: subprocess.Popen[str] | None = None + try: + with log_path.open("w", encoding = "utf-8", errors = "replace") as log_handle: + process = subprocess.Popen( + command, + stdout = log_handle, + stderr = subprocess.STDOUT, + text = True, + env = binary_env( + server_path, install_dir, host, runtime_line = runtime_line + ), + ) + deadline = time.time() + 20 + startup_started = time.time() + response_body = "" + last_error: Exception | None = None + while time.time() < deadline: + if process.poll() is not None: + process.wait(timeout = 5) + log_handle.flush() + output = read_log_excerpt(log_path) + exited_quickly = ( + time.time() - startup_started + ) <= SERVER_BIND_RETRY_WINDOW_SECONDS + failure = PrebuiltFallback( + "llama-server exited during startup:\n" + output + ) + if ( + port_attempt < SERVER_PORT_BIND_ATTEMPTS + and is_retryable_server_bind_error( + last_error, + output, + exited_quickly = exited_quickly, + ) + ): + log( + f"llama-server startup hit a port race on {port}; retrying with a fresh port " + f"({port_attempt}/{SERVER_PORT_BIND_ATTEMPTS})" + ) + last_failure = failure + break + raise failure + + payload = json.dumps({"prompt": "a", "n_predict": 1}).encode( + "utf-8" + ) + request = urllib.request.Request( + f"http://127.0.0.1:{port}/completion", + data = payload, + headers = {"Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(request, timeout = 5) as response: + status_code = response.status + response_body = response.read().decode("utf-8", "replace") + if status_code == 200: + return + last_error = RuntimeError( + f"unexpected HTTP status {status_code}" + ) + except urllib.error.HTTPError as exc: + response_body = exc.read().decode("utf-8", "replace") + last_error = exc + except Exception as exc: + last_error = exc + time.sleep(0.5) + else: + log_handle.flush() + output = read_log_excerpt(log_path) + raise PrebuiltFallback( + "llama-server completion validation timed out" + + (f" ({last_error})" if last_error else "") + + ":\n" + + output + + ("\n" + response_body if response_body else "") + ) + finally: + if process is not None and process.poll() is None: + process.terminate() + try: + process.wait(timeout = 5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout = 5) + try: + log_path.unlink(missing_ok = True) + except Exception: + pass + if last_failure is not None: + raise last_failure + raise PrebuiltFallback("llama-server validation failed unexpectedly") + + +def collect_system_report( + host: HostInfo, choice: AssetChoice | None, install_dir: Path +) -> str: + lines = [ + f"platform={host.system} machine={host.machine}", + f"driver_cuda_version={host.driver_cuda_version}", + f"compute_caps={','.join(host.compute_caps) if host.compute_caps else 'unknown'}", + f"cuda_visible_devices={host.visible_cuda_devices if host.visible_cuda_devices is not None else 'unset'}", + f"has_physical_nvidia={host.has_physical_nvidia}", + f"has_usable_nvidia={host.has_usable_nvidia}", + f"chosen_asset={(choice.name if choice else 'none')}", + f"asset_source={(choice.source_label if choice else 'none')}", + ] + if host.is_linux and host.has_physical_nvidia: + runtime_lines, runtime_dirs = detected_linux_runtime_lines() + lines.append( + "linux_runtime_lines=" + + (",".join(runtime_lines) if runtime_lines else "none") + ) + for runtime_line in ("cuda13", "cuda12"): + lines.append( + f"linux_runtime_dirs_{runtime_line}=" + + ( + ",".join(runtime_dirs.get(runtime_line, [])) + if runtime_dirs.get(runtime_line) + else "none" + ) + ) + if choice and choice.selection_log: + lines.append("selection_log:") + lines.extend(choice.selection_log) + if host.nvidia_smi: + try: + smi = run_capture([host.nvidia_smi], timeout = 20) + excerpt = "\n".join((smi.stdout + smi.stderr).splitlines()[:20]) + lines.append("nvidia-smi:") + lines.append(excerpt) + except Exception as exc: + lines.append(f"nvidia-smi error: {exc}") + + if host.is_linux: + server_binary = install_dir / "llama-server" + if server_binary.exists(): + server_env = binary_env(server_binary, install_dir, host) + lines.append( + "linux_missing_libs=" + + ( + ",".join(linux_missing_libraries(server_binary, env = server_env)) + or "none" + ) + ) + lines.append( + "linux_runtime_dirs=" + + ( + ",".join( + [ + part + for part in server_env.get("LD_LIBRARY_PATH", "").split( + os.pathsep + ) + if part + ] + ) + or "none" + ) + ) + try: + ldd = run_capture( + ["ldd", str(server_binary)], timeout = 20, env = server_env + ) + lines.append("ldd llama-server:") + lines.append((ldd.stdout + ldd.stderr).strip()) + except Exception as exc: + lines.append(f"ldd error: {exc}") + elif host.is_windows: + lines.append( + "windows_runtime_dirs=" + (",".join(windows_runtime_dirs()) or "none") + ) + runtime_lines, runtime_dirs = detected_windows_runtime_lines() + lines.append( + "windows_runtime_lines=" + + (",".join(runtime_lines) if runtime_lines else "none") + ) + for runtime_line in ("cuda13", "cuda12"): + lines.append( + f"windows_runtime_dirs_{runtime_line}=" + + ( + ",".join(runtime_dirs.get(runtime_line, [])) + if runtime_dirs.get(runtime_line) + else "none" + ) + ) + elif host.is_macos: + server_binary = install_dir / "llama-server" + if server_binary.exists(): + try: + otool = run_capture(["otool", "-L", str(server_binary)], timeout = 20) + lines.append("otool -L llama-server:") + lines.append((otool.stdout + otool.stderr).strip()) + except Exception as exc: + lines.append(f"otool error: {exc}") + + return "\n".join(lines) + + +def apply_approved_hashes( + attempts: Iterable[AssetChoice], + checksums: ApprovedReleaseChecksums, +) -> list[AssetChoice]: + approved_attempts: list[AssetChoice] = [] + missing_assets: list[str] = [] + for attempt in attempts: + approved = checksums.artifacts.get(attempt.name) + if approved is None: + missing_assets.append(attempt.name) + continue + attempt.expected_sha256 = approved.sha256 + approved_attempts.append(attempt) + if not approved_attempts: + missing_text = ", ".join(missing_assets) if missing_assets else "none" + raise PrebuiltFallback( + "approved checksum asset did not contain the selected prebuilt archive(s): " + f"{missing_text}" + ) + return approved_attempts + + +def require_approved_source_hash( + checksums: ApprovedReleaseChecksums, llama_tag: str +) -> ApprovedArtifactHash: + source_asset_name = source_archive_logical_name(llama_tag) + approved_source = checksums.artifacts.get(source_asset_name) + if approved_source is None: + raise PrebuiltFallback( + f"approved checksum asset did not contain source archive {source_asset_name}" + ) + return approved_source + + +def resolve_install_attempts( + llama_tag: str, + host: HostInfo, + published_repo: str, + published_release_tag: str, +) -> tuple[str, str, list[AssetChoice], ApprovedReleaseChecksums]: + requested_tag = llama_tag + resolved_tag = resolve_requested_install_tag(llama_tag, published_release_tag) + checksums = load_approved_release_checksums(published_repo, resolved_tag) + require_approved_source_hash(checksums, resolved_tag) + + if host.is_linux and host.is_x86_64 and host.has_usable_nvidia: + linux_cuda_selection = resolve_linux_cuda_choice( + host, resolved_tag, published_repo, published_release_tag + ) + attempts = apply_approved_hashes(linux_cuda_selection.attempts, checksums) + if not attempts: + raise PrebuiltFallback("no compatible Linux CUDA asset was found") + log_lines(linux_cuda_selection.selection_log) + return requested_tag, resolved_tag, attempts, checksums + + if host.is_windows and host.is_x86_64 and host.has_usable_nvidia: + upstream_assets = github_release_assets(UPSTREAM_REPO, resolved_tag) + attempts = apply_approved_hashes( + resolve_windows_cuda_choices(host, resolved_tag, upstream_assets), checksums + ) + if not attempts: + raise PrebuiltFallback("no compatible Windows CUDA asset was found") + if attempts[0].selection_log: + log_lines(attempts[0].selection_log) + return requested_tag, resolved_tag, attempts, checksums + + choice = resolve_asset_choice( + host, resolved_tag, published_repo, published_release_tag + ) + approved_attempts = apply_approved_hashes([choice], checksums) + if choice.selection_log: + log_lines(choice.selection_log) + return requested_tag, resolved_tag, approved_attempts, checksums + + +def write_prebuilt_metadata( + install_dir: Path, + *, + requested_tag: str, + llama_tag: str, + choice: AssetChoice, + prebuilt_fallback_used: bool, +) -> None: + metadata = { + "requested_tag": requested_tag, + "tag": llama_tag, + "asset": choice.name, + "source": choice.source_label, + "bundle_profile": choice.bundle_profile, + "runtime_line": choice.runtime_line, + "coverage_class": choice.coverage_class, + "prebuilt_fallback_used": prebuilt_fallback_used, + "installed_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + (install_dir / "UNSLOTH_PREBUILT_INFO.json").write_text( + json.dumps(metadata, indent = 2) + "\n" + ) + + +def validate_prebuilt_choice( + choice: AssetChoice, + host: HostInfo, + install_dir: Path, + work_dir: Path, + probe_path: Path, + *, + requested_tag: str, + llama_tag: str, + approved_checksums: ApprovedReleaseChecksums, + prebuilt_fallback_used: bool, + quantized_path: Path, +) -> tuple[Path, Path]: + source_archive = approved_checksums.artifacts.get( + source_archive_logical_name(llama_tag) + ) + if source_archive is None: + raise PrebuiltFallback( + f"approved checksum asset did not contain source archive {source_archive_logical_name(llama_tag)}" + ) + log(f"hydrating upstream llama.cpp source for {llama_tag} into {install_dir}") + hydrate_source_tree( + llama_tag, + install_dir, + work_dir, + expected_sha256 = source_archive.sha256, + ) + log(f"overlaying prebuilt bundle {choice.name} into {install_dir}") + server_path, quantize_path = install_from_archives( + choice, host, install_dir, work_dir + ) + preflight_linux_installed_binaries((server_path, quantize_path), install_dir, host) + ensure_repo_shape(install_dir) + write_prebuilt_metadata( + install_dir, + requested_tag = requested_tag, + llama_tag = llama_tag, + choice = choice, + prebuilt_fallback_used = prebuilt_fallback_used, + ) + validate_quantize( + quantize_path, + probe_path, + quantized_path, + install_dir, + host, + runtime_line = choice.runtime_line, + ) + validate_server( + server_path, + probe_path, + host, + install_dir, + runtime_line = choice.runtime_line, + ) + log(f"staged prebuilt validation succeeded for {choice.name}") + return server_path, quantize_path + + +def validate_prebuilt_attempts( + attempts: Iterable[AssetChoice], + host: HostInfo, + install_dir: Path, + work_dir: Path, + probe_path: Path, + *, + requested_tag: str, + llama_tag: str, + approved_checksums: ApprovedReleaseChecksums, +) -> tuple[AssetChoice, Path, bool]: + attempt_list = list(attempts) + if not attempt_list: + raise PrebuiltFallback("no prebuilt bundle attempts were available") + + tried_fallback = False + for index, attempt in enumerate(attempt_list): + if index > 0: + tried_fallback = True + log( + "retrying CUDA prebuilt " + f"{attempt.name} install_kind={attempt.install_kind} " + f"runtime_line={attempt.runtime_line} coverage_class={attempt.coverage_class}" + ) + + staging_dir = create_install_staging_dir(install_dir) + quantized_path = work_dir / f"stories260K-q4-{index}.gguf" + if quantized_path.exists(): + quantized_path.unlink() + try: + validate_prebuilt_choice( + attempt, + host, + staging_dir, + work_dir, + probe_path, + requested_tag = requested_tag, + llama_tag = llama_tag, + approved_checksums = approved_checksums, + prebuilt_fallback_used = tried_fallback, + quantized_path = quantized_path, + ) + except Exception as exc: + remove_tree(staging_dir) + prune_install_staging_root(install_dir) + if isinstance(exc, PrebuiltFallback): + attempt_error = exc + else: + attempt_error = PrebuiltFallback( + f"candidate attempt failed before activation for {attempt.name}: {exc}" + ) + if index == len(attempt_list) - 1: + raise attempt_error from exc + log( + "selected CUDA bundle failed before activation; trying next prebuilt fallback " + f"({textwrap.shorten(str(attempt_error), width = 200, placeholder = '...')})" + ) + continue + + return attempt, staging_dir, tried_fallback + + raise PrebuiltFallback("no prebuilt bundle passed validation") + + +def install_prebuilt( + install_dir: Path, llama_tag: str, published_repo: str, published_release_tag: str +) -> None: + host = detect_host() + choice: AssetChoice | None = None + try: + with install_lock(install_lock_path(install_dir)): + if install_dir.exists(): + log( + f"existing llama.cpp install detected at {install_dir}; validating staged prebuilt update before replacement" + ) + else: + log( + f"no existing llama.cpp install detected at {install_dir}; performing fresh prebuilt install" + ) + requested_tag, llama_tag, attempts, approved_checksums = ( + resolve_install_attempts( + llama_tag, + host, + published_repo, + published_release_tag, + ) + ) + choice = attempts[0] + log( + f"selected {choice.name} ({choice.source_label}) for {host.system} {host.machine}" + ) + with tempfile.TemporaryDirectory(prefix = "unsloth-llama-prebuilt-") as tmp: + work_dir = Path(tmp) + probe_path = work_dir / "stories260K.gguf" + download_validation_model( + probe_path, validation_model_cache_path(install_dir) + ) + choice, selected_staging_dir, _ = validate_prebuilt_attempts( + attempts, + host, + install_dir, + work_dir, + probe_path, + requested_tag = requested_tag, + llama_tag = llama_tag, + approved_checksums = approved_checksums, + ) + activate_install_tree(selected_staging_dir, install_dir, host) + try: + ensure_converter_scripts(install_dir, llama_tag) + except Exception as exc: + log( + "converter script fetch failed after activation; install remains valid " + f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})" + ) + except PrebuiltFallback as exc: + log("prebuilt install path failed; falling back to source build") + log(f"prebuilt fallback reason: {exc}") + report = collect_system_report(host, choice, install_dir) + print(report) + raise SystemExit(EXIT_FALLBACK) from exc + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description = "Install and validate a prebuilt llama.cpp bundle for Unsloth Studio." + ) + parser.add_argument("--install-dir", help = "Target ~/.unsloth/llama.cpp directory") + parser.add_argument( + "--llama-tag", + default = DEFAULT_LLAMA_TAG, + help = f"llama.cpp release tag. Prebuilt installs are pinned to the approved tag {APPROVED_PREBUILT_LLAMA_TAG}.", + ) + parser.add_argument( + "--published-repo", + default = DEFAULT_PUBLISHED_REPO, + help = "Published bundle repository", + ) + parser.add_argument( + "--published-release-tag", + default = DEFAULT_PUBLISHED_TAG, + help = "Published GitHub release tag to pin. By default, scan releases until a compatible llama.cpp bundle is found.", + ) + resolve_group = parser.add_mutually_exclusive_group() + resolve_group.add_argument( + "--resolve-llama-tag", + nargs = "?", + const = "latest", + help = "Resolve a llama.cpp tag such as 'latest' to the logical upstream release tag.", + ) + resolve_group.add_argument( + "--resolve-install-tag", + nargs = "?", + const = "latest", + help = "Resolve a llama.cpp tag such as 'latest' to the concrete tag installable on the current host.", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if args.resolve_llama_tag is not None: + # Pass published_repo so the resolver prefers the Unsloth release tag + # (tested/approved) over the upstream ggml-org bleeding-edge tag. + print(resolve_requested_llama_tag(args.resolve_llama_tag, args.published_repo)) + return EXIT_SUCCESS + + if args.resolve_install_tag is not None: + print( + resolve_requested_install_tag( + args.resolve_install_tag, args.published_release_tag or "" + ) + ) + return EXIT_SUCCESS + + if not args.install_dir: + raise SystemExit( + "install_llama_prebuilt.py: --install-dir is required unless --resolve-llama-tag or --resolve-install-tag is used" + ) + install_prebuilt( + install_dir = Path(args.install_dir).expanduser().resolve(), + llama_tag = args.llama_tag, + published_repo = args.published_repo, + published_release_tag = args.published_release_tag or "", + ) + return EXIT_SUCCESS + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except SystemExit: + raise + except Exception as exc: + message = textwrap.shorten(str(exc), width = 400, placeholder = "...") + log(f"fatal helper error: {message}") + raise SystemExit(EXIT_ERROR) diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index a141c64425..39fec2e6f5 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -22,20 +22,20 @@ from pathlib import Path IS_WINDOWS = sys.platform == "win32" -# ── Verbosity control ────────────────────────────────────────────────────────── +# -- Verbosity control ---------------------------------------------------------- # By default the installer shows a minimal progress bar (one line, in-place). # Set UNSLOTH_VERBOSE=1 in the environment to restore full per-step output: # Linux/Mac: UNSLOTH_VERBOSE=1 ./studio/setup.sh # Windows: $env:UNSLOTH_VERBOSE="1" ; .\studio\setup.ps1 VERBOSE: bool = os.environ.get("UNSLOTH_VERBOSE", "0") == "1" -# Progress bar state — updated by _progress() as each install step runs. +# Progress bar state -- updated by _progress() as each install step runs. # _TOTAL counts: pip-upgrade + 7 shared steps + triton (non-Windows) + local-plugin + finalize # Update _TOTAL here if you add or remove install steps in install_python_stack(). _STEP: int = 0 _TOTAL: int = 0 # set at runtime in install_python_stack() based on platform -# ── Paths ────────────────────────────────────────────────────────────── +# -- Paths -------------------------------------------------------------- SCRIPT_DIR = Path(__file__).resolve().parent REQ_ROOT = SCRIPT_DIR / "backend" / "requirements" SINGLE_ENV = REQ_ROOT / "single-env" @@ -44,7 +44,39 @@ LOCAL_DD_UNSTRUCTURED_PLUGIN = ( SCRIPT_DIR / "backend" / "plugins" / "data-designer-unstructured-seed" ) -# ── Color support ────────────────────────────────────────────────────── +# -- Unicode-safe printing --------------------------------------------- +# On Windows the default console encoding can be a legacy code page +# (e.g. CP1252) that cannot represent Unicode glyphs such as ✅ or ❌. +# _safe_print() gracefully degrades to ASCII equivalents so the +# installer never crashes just because of a status glyph. + +_UNICODE_TO_ASCII: dict[str, str] = { + "\u2705": "[OK]", # ✅ + "\u274c": "[FAIL]", # ❌ + "\u26a0\ufe0f": "[!]", # ⚠️ (warning + variation selector) + "\u26a0": "[!]", # ⚠ (warning without variation selector) +} + + +def _safe_print(*args: object, **kwargs: object) -> None: + """Drop-in print() replacement that survives non-UTF-8 consoles.""" + try: + print(*args, **kwargs) + except UnicodeEncodeError: + # Stringify, then swap emoji for ASCII equivalents + text = " ".join(str(a) for a in args) + for uni, ascii_alt in _UNICODE_TO_ASCII.items(): + text = text.replace(uni, ascii_alt) + # Final fallback: replace any remaining unencodable chars + print( + text.encode(sys.stdout.encoding or "ascii", errors = "replace").decode( + sys.stdout.encoding or "ascii", errors = "replace" + ), + **kwargs, + ) + + +# -- Color support ------------------------------------------------------ def _enable_colors() -> bool: @@ -72,7 +104,7 @@ def _enable_colors() -> bool: return True # Unix terminals support ANSI by default -# Colors disabled — Colab and most CI runners render ANSI fine, but plain output +# Colors disabled -- Colab and most CI runners render ANSI fine, but plain output # is cleaner in the notebook cell. Re-enable by setting _HAS_COLOR = _enable_colors() _HAS_COLOR = False @@ -92,7 +124,7 @@ def _red(msg: str) -> str: def _progress(label: str) -> None: """Print an in-place progress bar for the current install step. - Uses only stdlib (sys.stdout) — no extra packages required. + Uses only stdlib (sys.stdout) -- no extra packages required. In VERBOSE mode this is a no-op; per-step labels are printed by run() instead. """ global _STEP @@ -119,7 +151,7 @@ def run( stderr = subprocess.STDOUT if quiet else None, ) if result.returncode != 0: - print(_red(f"❌ {label} failed (exit code {result.returncode}):")) + _safe_print(_red(f"❌ {label} failed (exit code {result.returncode}):")) if result.stdout: print(result.stdout.decode(errors = "replace")) sys.exit(result.returncode) @@ -129,7 +161,7 @@ def run( # Packages to skip on Windows (require special build steps) WINDOWS_SKIP_PACKAGES = {"open_spiel", "triton_kernels"} -# ── uv bootstrap ────────────────────────────────────────────────────── +# -- uv bootstrap ------------------------------------------------------ USE_UV = False # Set by _bootstrap_uv() at the start of install_python_stack() UV_NEEDS_SYSTEM = False # Set by _bootstrap_uv() via probe @@ -193,9 +225,20 @@ def _translate_pip_args_for_uv(args: tuple[str, ...]) -> list[str]: def _build_pip_cmd(args: tuple[str, ...]) -> list[str]: - """Build a standard pip install command.""" + """Build a standard pip install command. + + Strips uv-only flags like --upgrade-package that pip doesn't understand. + """ cmd = [sys.executable, "-m", "pip", "install"] - cmd.extend(args) + skip_next = False + for arg in args: + if skip_next: + skip_next = False + continue + if arg == "--upgrade-package": + skip_next = True # skip the flag and its value + continue + cmd.append(arg) return cmd @@ -209,7 +252,12 @@ def _build_uv_cmd(args: tuple[str, ...]) -> list[str]: # the system Python (observed on Colab and similar environments). cmd.extend(["--python", sys.executable]) cmd.extend(_translate_pip_args_for_uv(args)) - cmd.append("--torch-backend=auto") + # Torch is pre-installed by install.sh/setup.ps1. Do not add + # --torch-backend by default -- it can cause solver dead-ends on + # CPU-only machines. Callers that need it can set UV_TORCH_BACKEND. + _tb = os.environ.get("UV_TORCH_BACKEND", "") + if _tb: + cmd.append(f"--torch-backend={_tb}") return cmd @@ -267,7 +315,9 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: text = True, ) if result.returncode != 0: - print(_red(f" ⚠️ Could not find package {package_name}, skipping patch")) + _safe_print( + _red(f" ⚠️ Could not find package {package_name}, skipping patch") + ) return location = None @@ -277,7 +327,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: break if not location: - print(_red(f" ⚠️ Could not determine location of {package_name}")) + _safe_print(_red(f" ⚠️ Could not determine location of {package_name}")) return dest = Path(location) / relative_path @@ -285,28 +335,96 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: download_file(url, dest) -# ── Main install sequence ───────────────────────────────────────────── +# -- Main install sequence --------------------------------------------- def install_python_stack() -> int: global USE_UV, _STEP, _TOTAL _STEP = 0 - _TOTAL = 10 if IS_WINDOWS else 11 - # 1. Upgrade pip (needed even with uv as fallback and for bootstrapping) - _progress("pip upgrade") - run("Upgrading pip", [sys.executable, "-m", "pip", "install", "--upgrade", "pip"]) + # When called from install.sh (which already installed unsloth into the venv), + # SKIP_STUDIO_BASE=1 is set to avoid redundant reinstallation of base packages. + # When called from "unsloth studio update", it is NOT set so base packages + # (unsloth + unsloth-zoo) are always reinstalled to pick up new versions. + skip_base = os.environ.get("SKIP_STUDIO_BASE", "0") == "1" + # When --package is used, install a different package name (e.g. roland-sloth for testing) + package_name = os.environ.get("STUDIO_PACKAGE_NAME", "unsloth") + # When --local is used, overlay a local repo checkout after updating deps + local_repo = os.environ.get("STUDIO_LOCAL_REPO", "") + base_total = 10 if IS_WINDOWS else 11 + _TOTAL = (base_total - 1) if skip_base else base_total - # Try to use uv for faster installs + # 1. Try to use uv for faster installs (must happen before pip upgrade + # because uv venvs don't include pip by default) USE_UV = _bootstrap_uv() - # 2. Core packages: unsloth-zoo + unsloth - _progress("base packages") - pip_install( - "Installing base packages", - "--no-cache-dir", - req = REQ_ROOT / "base.txt", - ) + # 2. Ensure pip is available (uv venvs created by install.sh don't include pip) + _progress("pip bootstrap") + if USE_UV: + run( + "Bootstrapping pip via uv", + [ + "uv", + "pip", + "install", + "--python", + sys.executable, + "pip", + ], + ) + else: + run( + "Upgrading pip", + [sys.executable, "-m", "pip", "install", "--upgrade", "pip"], + ) + + # 3. Core packages: unsloth-zoo + unsloth (or custom package name) + if skip_base: + print(_green(f"✅ {package_name} already installed — skipping base packages")) + elif local_repo: + # Local dev install: update deps from base.txt, then overlay the + # local checkout as an editable install (--no-deps so torch is + # never re-resolved). + _progress("base packages") + pip_install( + "Updating base packages", + "--no-cache-dir", + "--upgrade-package", + "unsloth", + "--upgrade-package", + "unsloth-zoo", + req = REQ_ROOT / "base.txt", + ) + pip_install( + "Overlaying local repo (editable)", + "--no-cache-dir", + "--no-deps", + "-e", + local_repo, + constrain = False, + ) + elif package_name != "unsloth": + # Custom package name (e.g. roland-sloth for testing) — install directly + _progress("base packages") + pip_install( + f"Installing {package_name}", + "--no-cache-dir", + package_name, + ) + else: + # Update path: upgrade only unsloth + unsloth-zoo while preserving + # existing torch/CUDA installations. Torch is pre-installed by + # install.sh / setup.ps1; --upgrade-package targets only base pkgs. + _progress("base packages") + pip_install( + "Updating base packages", + "--no-cache-dir", + "--upgrade-package", + "unsloth", + "--upgrade-package", + "unsloth-zoo", + req = REQ_ROOT / "base.txt", + ) # 3. Extra dependencies _progress("unsloth extras") @@ -316,7 +434,7 @@ def install_python_stack() -> int: req = REQ_ROOT / "extras.txt", ) - # 3b. Extra dependencies (no-deps) — audio model support etc. + # 3b. Extra dependencies (no-deps) -- audio model support etc. _progress("extra codecs") pip_install( "Installing extras (no-deps)", @@ -325,7 +443,7 @@ def install_python_stack() -> int: req = REQ_ROOT / "extras-no-deps.txt", ) - # 4. Overrides (torchao, transformers) — force-reinstall + # 4. Overrides (torchao, transformers) -- force-reinstall _progress("dependency overrides") pip_install( "Installing dependency overrides", @@ -393,7 +511,7 @@ def install_python_stack() -> int: # 11. Local Data Designer seed plugin if not LOCAL_DD_UNSTRUCTURED_PLUGIN.is_dir(): - print( + _safe_print( _red( f"❌ Missing local plugin directory: {LOCAL_DD_UNSTRUCTURED_PLUGIN}", ), @@ -422,7 +540,7 @@ def install_python_stack() -> int: stderr = subprocess.DEVNULL, ) - print(_green("✅ Python dependencies installed")) + _safe_print(_green("✅ Python dependencies installed")) return 0 diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 8966449423..0ac54d3866 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -250,9 +250,15 @@ function Find-VsBuildTools { # ───────────────────────────────────────────── # Banner # ───────────────────────────────────────────── -Write-Host "+==============================================+" -ForegroundColor Green -Write-Host "| Unsloth Studio Setup (Windows) |" -ForegroundColor Green -Write-Host "+==============================================+" -ForegroundColor Green +if ($env:SKIP_STUDIO_BASE -eq "1") { + Write-Host "+==============================================+" -ForegroundColor Green + Write-Host "| Unsloth Studio Setup (Windows) |" -ForegroundColor Green + Write-Host "+==============================================+" -ForegroundColor Green +} else { + Write-Host "+==============================================+" -ForegroundColor Green + Write-Host "| Unsloth Studio Update (Windows) |" -ForegroundColor Green + Write-Host "+==============================================+" -ForegroundColor Green +} # ========================================================================== # PHASE 1: System-level prerequisites (winget installs, env vars) @@ -497,7 +503,6 @@ if ($DriverMaxCuda) { $isCompat = ($tkMaj -lt $drMajorCuda) -or ($tkMaj -eq $drMajorCuda -and $tkMin -le $drMinorCuda) if ($isCompat) { # Also verify the toolkit supports our GPU architecture - Write-Host " [DEBUG] Checking CUDA compatibility: toolkit=$tkMaj.$tkMin arch=sm_$CudaArch" -ForegroundColor Magenta $archOk = $true if ($CudaArch) { $archOk = Test-NvccArchSupport -NvccExe $candidateNvcc -Arch $CudaArch @@ -728,16 +733,22 @@ if ($IsPipInstall) { Write-Host "[OK] Running from pip install - frontend already bundled, skipping Node/npm check" -ForegroundColor Green } else { # setup.sh installs Node LTS (v22) via nvm. We enforce the same range here: - # Node >= 20, npm >= 11. + # Vite 8 requires Node ^20.19.0 || >=22.12.0, npm >= 11. $NeedNode = $true try { $NodeVersion = (node -v 2>$null) $NpmVersion = (npm -v 2>$null) if ($NodeVersion -and $NpmVersion) { - $NodeMajor = [int]($NodeVersion -replace 'v','').Split('.')[0] + $NodeParts = ($NodeVersion -replace 'v','').Split('.') + $NodeMajor = [int]$NodeParts[0] + $NodeMinor = [int]$NodeParts[1] $NpmMajor = [int]$NpmVersion.Split('.')[0] - if ($NodeMajor -ge 20 -and $NpmMajor -ge 11) { + # Vite 8: ^20.19.0 || >=22.12.0 + $NodeOk = ($NodeMajor -eq 20 -and $NodeMinor -ge 19) -or + ($NodeMajor -eq 22 -and $NodeMinor -ge 12) -or + ($NodeMajor -ge 23) + if ($NodeOk -and $NpmMajor -ge 11) { Write-Host "[OK] Node $NodeVersion and npm $NpmVersion already meet requirements." -ForegroundColor Green $NeedNode = $false } else { @@ -761,6 +772,24 @@ if ($IsPipInstall) { } Write-Host "[OK] Node $(node -v) | npm $(npm -v)" -ForegroundColor Green + + # ── bun (optional, faster package installs) ── + # Installed via npm — Node is already guaranteed above. Works on all platforms. + if (-not (Get-Command bun -ErrorAction SilentlyContinue)) { + Write-Host " Installing bun (faster frontend package installs)..." -ForegroundColor DarkGray + $prevEAP_bun = $ErrorActionPreference + $ErrorActionPreference = "Continue" + npm install -g bun 2>&1 | Out-Null + $ErrorActionPreference = $prevEAP_bun + Refresh-Environment + if (Get-Command bun -ErrorAction SilentlyContinue) { + Write-Host "[OK] bun installed ($(bun --version))" -ForegroundColor Green + } else { + Write-Host "[OK] bun install skipped (npm will be used instead)" -ForegroundColor DarkGray + } + } else { + Write-Host "[OK] bun already installed ($(bun --version))" -ForegroundColor Green + } } # ============================================ @@ -844,10 +873,10 @@ if ($IsPipInstall) { if ($NewerFile) { break } } } - # Also check all top-level files (package.json, bun.lock, vite.config.ts, index.html, etc.) + # Also check all top-level files (package.json, vite.config.ts, index.html, etc.) if (-not $NewerFile) { $NewerFile = Get-ChildItem -Path $FrontendDir -File -ErrorAction SilentlyContinue | - Where-Object { $_.LastWriteTime -gt $DistTime } | + Where-Object { $_.Name -ne "bun.lock" -and $_.LastWriteTime -gt $DistTime } | Select-Object -First 1 } if (-not $NewerFile) { @@ -882,26 +911,73 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { $WalkDir = Split-Path $WalkDir -Parent } - # npm writes warnings to stderr; lower ErrorActionPreference so PS doesn't - # treat them as terminating errors (same pattern as the pip section below). + # Use bun if available (faster install), fall back to npm. + # Bun is used only as package manager; Node runs the actual build (Vite 8). $prevEAP_npm = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $FrontendDir - npm install 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { - Pop-Location - $ErrorActionPreference = $prevEAP_npm - foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } - Write-Host "[ERROR] npm install failed (exit code $LASTEXITCODE)" -ForegroundColor Red - Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow - exit 1 + + $UseBun = $null -ne (Get-Command bun -ErrorAction SilentlyContinue) + + # bun's package cache can become corrupt -- packages get stored with only + # metadata but no actual content (bin/, lib/). When this happens bun install + # exits 0 but leaves binaries missing. We validate after install and clear + # the cache + retry once before falling back to npm. + if ($UseBun) { + Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray + & bun install *> $null + $bunExit = $LASTEXITCODE + # On Windows, .bin/ entries can be tsc, tsc.cmd, or tsc.ps1 + $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") + $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") + if ($bunExit -eq 0 -and $hasTsc -and $hasVite) { + # bun install succeeded and critical binaries are present + } elseif ($bunExit -eq 0) { + Write-Host " bun install exited 0 but critical binaries are missing, clearing cache and retrying..." -ForegroundColor Yellow + if (Test-Path "node_modules") { + Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue + } + & bun pm cache rm *> $null + & bun install *> $null + $bunExit = $LASTEXITCODE + $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") + $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") + if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) { + Write-Host " bun retry failed, falling back to npm" -ForegroundColor Yellow + if (Test-Path "node_modules") { + Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue + } + $UseBun = $false + } + } else { + Write-Host " [WARN] bun install failed (exit $bunExit), falling back to npm" -ForegroundColor Yellow + if (Test-Path "node_modules") { + Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue + } + $UseBun = $false + } } - npm run build 2>&1 | Out-Null - if ($LASTEXITCODE -ne 0) { + if (-not $UseBun) { + & npm install *> $null + $npmExit = $LASTEXITCODE + if ($npmExit -ne 0) { + Pop-Location + $ErrorActionPreference = $prevEAP_npm + foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } + Write-Host "[ERROR] npm install failed (exit code $npmExit)" -ForegroundColor Red + Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow + exit 1 + } + } + + # Always use npm to run the build (Node runtime — avoids bun Windows runtime issues) + & npm run build *> $null + $buildExit = $LASTEXITCODE + if ($buildExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } - Write-Host "[ERROR] npm run build failed (exit code $LASTEXITCODE)" -ForegroundColor Red + Write-Host "[ERROR] npm run build failed (exit code $buildExit)" -ForegroundColor Red exit 1 } Pop-Location @@ -1030,9 +1106,9 @@ if (-not $PythonCmd) { Write-Host "[OK] Using $PythonCmd ($(& $PythonCmd --version 2>&1))" -ForegroundColor Green -# Always create a .venv for isolation -- even for pip installs. -# Created in the repo root (parent of studio/). -$VenvDir = Join-Path $env:USERPROFILE ".unsloth\studio\.venv" +# The venv must already exist (created by install.ps1). +# This script (setup.ps1 / "unsloth studio update") only updates packages. +$VenvDir = Join-Path $env:USERPROFILE ".unsloth\studio\unsloth_studio" # Stale-venv detection: if the venv exists but its torch flavor no longer # matches the current machine, wipe it so we get a clean install. @@ -1095,8 +1171,10 @@ if (Test-Path $VenvDir -PathType Container) { } if (-not (Test-Path $VenvDir)) { - Write-Host " Creating virtual environment at $VenvDir..." -ForegroundColor Cyan - & $PythonCmd -m venv $VenvDir + Write-Host "[ERROR] Virtual environment not found at $VenvDir" -ForegroundColor Red + Write-Host " Run install.ps1 first to create the environment:" -ForegroundColor Yellow + Write-Host " irm https://unsloth.ai/install.ps1 | iex" -ForegroundColor Yellow + exit 1 } else { Write-Host " Reusing existing virtual environment at $VenvDir" -ForegroundColor Green } @@ -1243,6 +1321,96 @@ if ($LASTEXITCODE -ne 0) { $ErrorActionPreference = $prevEAP_t5 Write-Host "[OK] Transformers 5.x pre-installed to .venv_t5/" -ForegroundColor Green +# ========================================================================== +# PHASE 3.4: Prefer prebuilt llama.cpp bundles before source build +# ========================================================================== +$UnslothHome = Join-Path $env:USERPROFILE ".unsloth" +if (-not (Test-Path $UnslothHome)) { New-Item -ItemType Directory -Force $UnslothHome | Out-Null } +$LlamaCppDir = Join-Path $UnslothHome "llama.cpp" +$NeedLlamaSourceBuild = $false +$SkipPrebuiltInstall = $false +$RequestedLlamaTag = if ($env:UNSLOTH_LLAMA_TAG) { $env:UNSLOTH_LLAMA_TAG } else { "latest" } +$HelperReleaseRepo = if ($env:UNSLOTH_LLAMA_RELEASE_REPO) { $env:UNSLOTH_LLAMA_RELEASE_REPO } else { "unslothai/llama.cpp" } +$resolveOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-install-tag $RequestedLlamaTag --published-repo $HelperReleaseRepo 2>&1 +$resolveExit = $LASTEXITCODE +$ResolvedLlamaTag = if ($resolveOutput) { ($resolveOutput | Select-Object -Last 1).ToString().Trim() } else { "" } +if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) { + Write-Host "" + Write-Host "[WARN] Failed to resolve an installable prebuilt llama.cpp tag via $HelperReleaseRepo" -ForegroundColor Yellow + if ($resolveOutput) { + $resolveOutput | ForEach-Object { Write-Host $_ } + } + # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo + # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream + # bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp. + $fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-llama-tag $RequestedLlamaTag --published-repo $HelperReleaseRepo 2>$null + $fallbackExit = $LASTEXITCODE + $ResolvedLlamaTag = if ($fallbackExit -eq 0 -and $fallbackOutput) { + ($fallbackOutput | Select-Object -Last 1).ToString().Trim() + } elseif ($RequestedLlamaTag -eq "latest") { + # Try Unsloth release repo first, then fall back to ggml-org upstream + $resolvedLatest = $null + try { + $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/$HelperReleaseRepo/releases/latest" -ErrorAction Stop + $resolvedLatest = $latestRelease.tag_name + } catch {} + if (-not $resolvedLatest) { + try { + $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest" -ErrorAction Stop + $resolvedLatest = $latestRelease.tag_name + } catch {} + } + if ($resolvedLatest) { $resolvedLatest } else { $RequestedLlamaTag } + } else { + $RequestedLlamaTag + } + $NeedLlamaSourceBuild = $true + $SkipPrebuiltInstall = $true +} + +Write-Host "" +Write-Host "Resolved llama.cpp release tag: $ResolvedLlamaTag" -ForegroundColor Gray + +if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") { + Write-Host "" + Write-Host "[WARN] UNSLOTH_LLAMA_FORCE_COMPILE=1 -- skipping prebuilt llama.cpp install" -ForegroundColor Yellow + $NeedLlamaSourceBuild = $true +} else { + Write-Host "" + Write-Host "Installing prebuilt llama.cpp bundle (preferred path)..." -ForegroundColor Cyan + if (Test-Path $LlamaCppDir) { + Write-Host "Existing llama.cpp install detected -- validating staged prebuilt update before replacement" -ForegroundColor Gray + } + if ($SkipPrebuiltInstall) { + Write-Host "[WARN] Skipping prebuilt install because prebuilt tag resolution failed -- falling back to source build" -ForegroundColor Yellow + } else { + $prebuiltArgs = @( + "$PSScriptRoot\install_llama_prebuilt.py", + "--install-dir", $LlamaCppDir, + "--llama-tag", $ResolvedLlamaTag, + "--published-repo", $HelperReleaseRepo + ) + if ($env:UNSLOTH_LLAMA_RELEASE_TAG) { + $prebuiltArgs += @("--published-release-tag", $env:UNSLOTH_LLAMA_RELEASE_TAG) + } + $prevEAPPrebuilt = $ErrorActionPreference + $ErrorActionPreference = "Continue" + & python @prebuiltArgs + $prebuiltExit = $LASTEXITCODE + $ErrorActionPreference = $prevEAPPrebuilt + + if ($prebuiltExit -eq 0) { + Write-Host "[OK] Prebuilt llama.cpp installed and validated" -ForegroundColor Green + } else { + if (Test-Path $LlamaCppDir) { + Write-Host "[WARN] Prebuilt update failed; existing install was restored or cleaned before source build fallback" -ForegroundColor Yellow + } + Write-Host "[WARN] Prebuilt llama.cpp path unavailable or failed validation -- falling back to source build" -ForegroundColor Yellow + $NeedLlamaSourceBuild = $true + } + } +} + # ========================================================================== # PHASE 3.5: Install OpenSSL dev (for HTTPS support in llama-server) # ========================================================================== @@ -1250,42 +1418,46 @@ Write-Host "[OK] Transformers 5.x pre-installed to .venv_t5/" -ForegroundColor G # ShiningLight.OpenSSL.Dev includes headers + libs that cmake can find. $OpenSslAvailable = $false -# Check if OpenSSL dev is already installed (look for include dir) -$OpenSslRoots = @( - 'C:\Program Files\OpenSSL-Win64', - 'C:\Program Files\OpenSSL', - 'C:\OpenSSL-Win64' -) -$OpenSslRoot = $null -foreach ($root in $OpenSslRoots) { - if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { - $OpenSslRoot = $root - break - } -} - -if ($OpenSslRoot) { - $OpenSslAvailable = $true - Write-Host "[OK] OpenSSL dev found at $OpenSslRoot" -ForegroundColor Green -} else { - Write-Host "" - Write-Host "Installing OpenSSL dev (for HTTPS in llama-server)..." -ForegroundColor Cyan - $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) - if ($HasWinget) { - winget install -e --id ShiningLight.OpenSSL.Dev --accept-package-agreements --accept-source-agreements - # Re-check after install - foreach ($root in $OpenSslRoots) { - if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { - $OpenSslRoot = $root - $OpenSslAvailable = $true - Write-Host "[OK] OpenSSL dev installed at $OpenSslRoot" -ForegroundColor Green - break - } +if ($NeedLlamaSourceBuild) { + # Check if OpenSSL dev is already installed (look for include dir) + $OpenSslRoots = @( + 'C:\Program Files\OpenSSL-Win64', + 'C:\Program Files\OpenSSL', + 'C:\OpenSSL-Win64' + ) + $OpenSslRoot = $null + foreach ($root in $OpenSslRoots) { + if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { + $OpenSslRoot = $root + break } } - if (-not $OpenSslAvailable) { - Write-Host "[WARN] OpenSSL dev not available -- llama-server will be built without HTTPS" -ForegroundColor Yellow + + if ($OpenSslRoot) { + $OpenSslAvailable = $true + Write-Host "[OK] OpenSSL dev found at $OpenSslRoot" -ForegroundColor Green + } else { + Write-Host "" + Write-Host "Installing OpenSSL dev (for HTTPS in llama-server)..." -ForegroundColor Cyan + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + winget install -e --id ShiningLight.OpenSSL.Dev --accept-package-agreements --accept-source-agreements + # Re-check after install + foreach ($root in $OpenSslRoots) { + if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { + $OpenSslRoot = $root + $OpenSslAvailable = $true + Write-Host "[OK] OpenSSL dev installed at $OpenSslRoot" -ForegroundColor Green + break + } + } + } + if (-not $OpenSslAvailable) { + Write-Host "[WARN] OpenSSL dev not available -- llama-server will be built without HTTPS" -ForegroundColor Yellow + } } +} else { + Write-Host "[SKIP] OpenSSL dev install -- prebuilt llama.cpp already validated" -ForegroundColor Yellow } # ========================================================================== @@ -1298,9 +1470,7 @@ if ($OpenSslRoot) { # - llama-server: for GGUF model inference (with HTTPS if OpenSSL available) # - llama-quantize: for GGUF export quantization # Prerequisites (git, cmake, VS Build Tools, CUDA Toolkit) already installed in Phase 1. -$UnslothHome = Join-Path $env:USERPROFILE ".unsloth" -if (-not (Test-Path $UnslothHome)) { New-Item -ItemType Directory -Force $UnslothHome | Out-Null } -$LlamaCppDir = Join-Path $UnslothHome "llama.cpp" +$OriginalLlamaCppDir = $LlamaCppDir $BuildDir = Join-Path $LlamaCppDir "build" $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe" @@ -1323,7 +1493,10 @@ if (Test-Path $LlamaServerBin) { } } -if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) { +if (-not $NeedLlamaSourceBuild) { + Write-Host "" + Write-Host "[OK] Using validated prebuilt llama.cpp install at $LlamaCppDir" -ForegroundColor Green +} elseif ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) { Write-Host "" Write-Host "[OK] llama-server already exists at $LlamaServerBin" -ForegroundColor Green } elseif (-not $HasCmakeForBuild) { @@ -1379,29 +1552,49 @@ if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) { # -- Step A: Clone or pull llama.cpp -- + $UseConcreteRef = ($ResolvedLlamaTag -ne "latest" -and -not [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) + if (Test-Path (Join-Path $LlamaCppDir ".git")) { - Write-Host " llama.cpp repo already cloned, pulling latest..." -ForegroundColor Gray - git -C $LlamaCppDir pull 2>&1 | Out-Null + Write-Host " Syncing llama.cpp to $ResolvedLlamaTag..." -ForegroundColor Gray + if ($UseConcreteRef) { + git -C $LlamaCppDir fetch --depth 1 origin $ResolvedLlamaTag 2>&1 | Out-Null + } else { + git -C $LlamaCppDir fetch --depth 1 origin 2>&1 | Out-Null + } if ($LASTEXITCODE -ne 0) { - Write-Host " [WARN] git pull failed -- using existing source" -ForegroundColor Yellow + Write-Host " [WARN] git fetch failed -- using existing source" -ForegroundColor Yellow + } else { + git -C $LlamaCppDir checkout -B unsloth-llama-build FETCH_HEAD 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + $BuildOk = $false + $FailedStep = "git checkout" + } else { + git -C $LlamaCppDir clean -fdx 2>&1 | Out-Null + } } } else { - Write-Host " Cloning llama.cpp..." -ForegroundColor Gray - if (Test-Path $LlamaCppDir) { Remove-Item -Recurse -Force $LlamaCppDir } - git clone --depth 1 https://github.com/ggml-org/llama.cpp.git $LlamaCppDir 2>&1 | Out-Null + Write-Host " Cloning llama.cpp @ $ResolvedLlamaTag..." -ForegroundColor Gray + $buildTmp = "$LlamaCppDir.build.$PID" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + $cloneArgs = @("clone", "--depth", "1") + if ($UseConcreteRef) { + $cloneArgs += @("--branch", $ResolvedLlamaTag) + } + $cloneArgs += @("https://github.com/ggml-org/llama.cpp.git", $buildTmp) + git @cloneArgs 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { $BuildOk = $false $FailedStep = "git clone" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + } + # Use temp dir for build; swap into $LlamaCppDir only after build succeeds + if ($BuildOk) { + $LlamaCppDir = $buildTmp + $BuildDir = Join-Path $LlamaCppDir "build" } } # -- Step B: cmake configure -- - # Clean stale CMake cache to prevent previous CUDA settings from leaking - # into a CPU-only rebuild (or vice versa). - $CmakeCacheFile = Join-Path $BuildDir "CMakeCache.txt" - if (Test-Path $CmakeCacheFile) { - Remove-Item -Recurse -Force $BuildDir - } if ($BuildOk) { Write-Host "" @@ -1502,6 +1695,21 @@ if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) { } } + # Swap temp build dir into final location (only if we built in a temp dir) + if ($BuildOk -and $LlamaCppDir -ne $OriginalLlamaCppDir) { + if (Test-Path $OriginalLlamaCppDir) { Remove-Item -Recurse -Force $OriginalLlamaCppDir } + Move-Item $LlamaCppDir $OriginalLlamaCppDir + $LlamaCppDir = $OriginalLlamaCppDir + $BuildDir = Join-Path $LlamaCppDir "build" + $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe" + } elseif (-not $BuildOk -and $LlamaCppDir -ne $OriginalLlamaCppDir) { + # Build failed -- clean up temp dir, preserve existing install + if (Test-Path $LlamaCppDir) { Remove-Item -Recurse -Force $LlamaCppDir } + $LlamaCppDir = $OriginalLlamaCppDir + $BuildDir = Join-Path $LlamaCppDir "build" + $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe" + } + # Restore ErrorActionPreference $ErrorActionPreference = $prevEAP @@ -1537,8 +1745,9 @@ if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) { # Done # ============================================ Write-Host "" +$doneLine = if ($env:SKIP_STUDIO_BASE -eq "1") { "Setup Complete!" } else { "Update Complete!" } Write-Host "+===============================================+" -ForegroundColor Green -Write-Host "| Setup Complete! |" -ForegroundColor Green +Write-Host "| $doneLine |" -ForegroundColor Green Write-Host "| |" -ForegroundColor Green Write-Host "| Launch with: |" -ForegroundColor Green Write-Host "| unsloth studio -H 0.0.0.0 -p 8888 |" -ForegroundColor Green diff --git a/studio/setup.sh b/studio/setup.sh index 851fcadc81..a7991b83be 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -44,9 +44,15 @@ run_quiet_no_exit() { _run_quiet return "$@" } -echo "╔══════════════════════════════════════╗" -echo "║ Unsloth Studio Setup Script ║" -echo "╚══════════════════════════════════════╝" +if [ "${SKIP_STUDIO_BASE:-0}" = "1" ]; then + echo "╔══════════════════════════════════════╗" + echo "║ Unsloth Studio Setup Script ║" + echo "╚══════════════════════════════════════╝" +else + echo "╔══════════════════════════════════════╗" + echo "║ Unsloth Studio Update Script ║" + echo "╚══════════════════════════════════════╝" +fi # ── Clean up stale Unsloth compiled caches ── rm -rf "$REPO_ROOT/unsloth_compiled_cache" @@ -69,6 +75,7 @@ _NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then # Check all top-level files (package.json, bun.lock, vite.config.ts, index.html, etc.) _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -type f \ + ! -name 'bun.lock' \ -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) # Check src/ and public/ recursively (|| true guards against set -e when dirs are missing) if [ -z "$_changed" ]; then @@ -85,12 +92,18 @@ else NEED_NODE=true if command -v node &>/dev/null && command -v npm &>/dev/null; then NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1) + NODE_MINOR=$(node -v | sed 's/v//' | cut -d. -f2) NPM_MAJOR=$(npm -v | cut -d. -f1) - if [ "$NODE_MAJOR" -ge 20 ] && [ "$NPM_MAJOR" -ge 11 ]; then + # Vite 8 requires Node ^20.19.0 || >=22.12.0 + NODE_OK=false + if [ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -ge 19 ]; then NODE_OK=true; fi + if [ "$NODE_MAJOR" -eq 22 ] && [ "$NODE_MINOR" -ge 12 ]; then NODE_OK=true; fi + if [ "$NODE_MAJOR" -ge 23 ]; then NODE_OK=true; fi + if [ "$NODE_OK" = true ] && [ "$NPM_MAJOR" -ge 11 ]; then echo "✅ Node $(node -v) and npm $(npm -v) already meet requirements. Skipping nvm install." NEED_NODE=false else - if [ "$IS_COLAB" = true ]; then + if [ "$IS_COLAB" = true ] && [ "$NODE_OK" = true ]; then echo "✅ Node $(node -v) and npm $(npm -v) detected in Colab." # In Colab, just upgrade npm directly - nvm doesn't work well if [ "$NPM_MAJOR" -lt 11 ]; then @@ -150,6 +163,20 @@ fi echo "✅ Node $(node -v) | npm $(npm -v)" +# ── Install bun (optional, faster package installs) ── +# Uses npm to install bun globally -- Node is already guaranteed above, +# avoids platform-specific installers, PATH issues, and admin requirements. +if ! command -v bun &>/dev/null; then + echo " Installing bun (faster frontend package installs)..." + if npm install -g bun > /dev/null 2>&1 && command -v bun &>/dev/null; then + echo " bun installed ($(bun --version))" + else + echo " bun install skipped (npm will be used instead)" + fi +else + echo " bun already installed ($(bun --version))" +fi + # ── 5. Build frontend ── cd "$SCRIPT_DIR/frontend" @@ -174,7 +201,57 @@ _restore_gitignores() { } trap _restore_gitignores EXIT -run_quiet "npm install" npm install +# Use bun for install if available (faster), fall back to npm. +# Build always uses npm (Node runtime -- avoids bun runtime issues on some platforms). +# NOTE: We intentionally avoid run_quiet for the bun install attempt because +# run_quiet calls exit on failure, which would kill the script before the npm +# fallback can run. Instead we capture output manually and only show it on failure. +# +# IMPORTANT: bun's package cache can become corrupt -- packages get stored +# with only metadata (package.json, README) but no actual content (bin/, +# lib/). When this happens bun install exits 0 but leaves binaries missing. +# We verify critical binaries after install. If missing, we clear the cache +# and retry once before falling back to npm. +_try_bun_install() { + local _log _exit_code=0 + _log=$(mktemp) + bun install >"$_log" 2>&1 || _exit_code=$? + + if [ "$_exit_code" -eq 0 ] && [ -x node_modules/.bin/tsc ] && [ -x node_modules/.bin/vite ]; then + rm -f "$_log" + return 0 + fi + + # Either bun install failed or it exited 0 but left packages missing + if [ "$_exit_code" -ne 0 ]; then + echo " bun install failed (exit code $_exit_code):" + else + echo " bun install exited 0 but critical binaries are missing:" + fi + sed 's/^/ | /' "$_log" >&2 + rm -f "$_log" + rm -rf node_modules + return 1 +} + +_bun_install_ok=false +if command -v bun &>/dev/null; then + echo " Using bun for package install (faster)" + if _try_bun_install; then + _bun_install_ok=true + else + # First attempt failed, likely due to corrupt cache entries. + # Clear the cache and retry once. + echo " Clearing bun cache and retrying..." + bun pm cache rm > /dev/null 2>&1 || true + if _try_bun_install; then + _bun_install_ok=true + fi + fi +fi +if [ "$_bun_install_ok" = false ]; then + run_quiet "npm install" npm install +fi run_quiet "npm run build" npm run build _restore_gitignores @@ -203,114 +280,31 @@ fi # ── 6. Python venv + deps ── -# ── 6a. Discover best Python >= 3.11 and < 3.14 (i.e. 3.11.x, 3.12.x, or 3.13.x) ── -MIN_PY_MINOR=11 # minimum minor version (>= 3.11) -MAX_PY_MINOR=13 # maximum minor version (< 3.14) -BEST_PY="" -BEST_MINOR=0 - -# If the caller (e.g. install.sh) already chose a Python, use it directly. -if [ -n "${REQUESTED_PYTHON_VERSION:-}" ] && [ -x "$REQUESTED_PYTHON_VERSION" ]; then - _req_ver=$("$REQUESTED_PYTHON_VERSION" --version 2>&1 | awk '{print $2}') - _req_major=$(echo "$_req_ver" | cut -d. -f1) - _req_minor=$(echo "$_req_ver" | cut -d. -f2) - if [ "$_req_major" -eq 3 ] 2>/dev/null && \ - [ "$_req_minor" -ge "$MIN_PY_MINOR" ] 2>/dev/null && \ - [ "$_req_minor" -le "$MAX_PY_MINOR" ] 2>/dev/null; then - BEST_PY="$REQUESTED_PYTHON_VERSION" - echo "Using requested Python version: $BEST_PY" - else - echo "Ignoring requested Python $REQUESTED_PYTHON_VERSION ($_req_ver) -- outside supported range" - fi -fi - -if [ -z "$BEST_PY" ]; then -# Collect candidate python3 binaries (python3, python3.9, python3.10, …) -for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)?$' | sort -u); do - if ! command -v "$candidate" &>/dev/null; then - continue - fi - # Get version string, e.g. "Python 3.12.5" - ver_str=$("$candidate" --version 2>&1) || continue - ver_str=$(echo "$ver_str" | awk '{print $2}') - py_major=$(echo "$ver_str" | cut -d. -f1) - py_minor=$(echo "$ver_str" | cut -d. -f2) - - # Skip anything that isn't Python 3 - if [ "$py_major" -ne 3 ] 2>/dev/null; then - continue - fi - - # Skip versions below 3.11 - if [ "$py_minor" -lt "$MIN_PY_MINOR" ] 2>/dev/null; then - continue - fi - - # Skip versions above 3.13 (require < 3.14) - if [ "$py_minor" -gt "$MAX_PY_MINOR" ] 2>/dev/null; then - continue - fi - - # Keep the highest qualifying version - if [ "$py_minor" -gt "$BEST_MINOR" ]; then - BEST_PY="$candidate" - BEST_MINOR="$py_minor" - fi -done -fi - -if [ -z "$BEST_PY" ]; then - echo "❌ ERROR: No Python version between 3.${MIN_PY_MINOR} and 3.${MAX_PY_MINOR} found on this system." - echo " Detected Python 3 installations:" - for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)?$' | sort -u); do - if command -v "$candidate" &>/dev/null; then - echo " - $candidate ($($candidate --version 2>&1))" - fi - done - echo "" - echo " Please install Python 3.${MIN_PY_MINOR} or 3.${MAX_PY_MINOR}." - echo " For example: sudo apt install python3.12 python3.12-venv" - exit 1 -fi - -BEST_VER=$("$BEST_PY" --version 2>&1 | awk '{print $2}') -echo "✅ Using $BEST_PY ($BEST_VER) — compatible (3.${MIN_PY_MINOR}.x – 3.${MAX_PY_MINOR}.x)" - -REQ_ROOT="$SCRIPT_DIR/backend/requirements" -SINGLE_ENV_CONSTRAINTS="$REQ_ROOT/single-env/constraints.txt" -SINGLE_ENV_DATA_DESIGNER="$REQ_ROOT/single-env/data-designer.txt" -SINGLE_ENV_DATA_DESIGNER_DEPS="$REQ_ROOT/single-env/data-designer-deps.txt" -SINGLE_ENV_PATCH="$REQ_ROOT/single-env/patch_metadata.py" - -install_python_stack() { - python "$SCRIPT_DIR/install_python_stack.py" -} - -# Create venv under ~/.unsloth/studio/ (shared location, not in repo). -# All platforms (including Colab) use the same isolated venv so that -# studio dependencies are never installed into the system Python. +# The venv must already exist (created by install.sh). +# This script (setup.sh / "unsloth studio update") only updates packages. STUDIO_HOME="$HOME/.unsloth/studio" -VENV_DIR="$STUDIO_HOME/.venv" +VENV_DIR="$STUDIO_HOME/unsloth_studio" VENV_T5_DIR="$STUDIO_HOME/.venv_t5" -mkdir -p "$STUDIO_HOME" # Clean up legacy in-repo venvs if they exist [ -d "$REPO_ROOT/.venv" ] && rm -rf "$REPO_ROOT/.venv" [ -d "$REPO_ROOT/.venv_overlay" ] && rm -rf "$REPO_ROOT/.venv_overlay" [ -d "$REPO_ROOT/.venv_t5" ] && rm -rf "$REPO_ROOT/.venv_t5" +# Note: do NOT delete $STUDIO_HOME/.venv here — install.sh handles migration -rm -rf "$VENV_DIR" -rm -rf "$VENV_T5_DIR" -# Try creating venv with pip; fall back to --without-pip + bootstrap -# (some environments like Colab have broken ensurepip) -if ! "$BEST_PY" -m venv "$VENV_DIR" 2>/dev/null; then - "$BEST_PY" -m venv --without-pip "$VENV_DIR" - source "$VENV_DIR/bin/activate" - curl -sS https://bootstrap.pypa.io/get-pip.py | python > /dev/null -else - source "$VENV_DIR/bin/activate" +if [ ! -x "$VENV_DIR/bin/python" ]; then + echo "❌ ERROR: Virtual environment not found at $VENV_DIR" + echo " Run install.sh first to create the environment:" + echo " curl -fsSL https://unsloth.ai/install.sh | sh" + exit 1 fi +source "$VENV_DIR/bin/activate" + +install_python_stack() { + python "$SCRIPT_DIR/install_python_stack.py" +} + # ── Ensure uv is available (much faster than pip) ── USE_UV=false if command -v uv &>/dev/null; then @@ -329,27 +323,149 @@ fast_install() { } cd "$SCRIPT_DIR" -install_python_stack -# ── 6b. Pre-install transformers 5.x into .venv_t5/ ── -# Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing -# at runtime (slow, ~10-15s), we pre-install into a separate directory. -# The training subprocess just prepends .venv_t5/ to sys.path -- instant switch. +# ── Check if Python deps need updating ── +# Compare installed package version against PyPI latest. +# Skip all Python dependency work if versions match (fast update path). +_PKG_NAME="${STUDIO_PACKAGE_NAME:-unsloth}" +_SKIP_PYTHON_DEPS=false +if [ "${SKIP_STUDIO_BASE:-0}" != "1" ] && [ "${STUDIO_LOCAL_INSTALL:-0}" != "1" ]; then + # Only check when NOT called from install.sh (which just installed the package) + INSTALLED_VER=$("$VENV_DIR/bin/python" -c " +from importlib.metadata import version +print(version('$_PKG_NAME')) +" 2>/dev/null || echo "") + + LATEST_VER=$(curl -fsSL --max-time 5 "https://pypi.org/pypi/$_PKG_NAME/json" 2>/dev/null \ + | "$VENV_DIR/bin/python" -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null \ + || echo "") + + if [ -n "$INSTALLED_VER" ] && [ -n "$LATEST_VER" ] && [ "$INSTALLED_VER" = "$LATEST_VER" ]; then + echo "✅ $_PKG_NAME $INSTALLED_VER is up to date (matches PyPI latest)" + _SKIP_PYTHON_DEPS=true + elif [ -n "$INSTALLED_VER" ] && [ -n "$LATEST_VER" ]; then + echo "⬆️ $_PKG_NAME $INSTALLED_VER → $LATEST_VER available, updating dependencies..." + elif [ -z "$LATEST_VER" ]; then + echo "⚠️ Could not reach PyPI, updating dependencies to be safe..." + fi +fi + +if [ "$_SKIP_PYTHON_DEPS" = false ]; then + install_python_stack + + # ── 6b. Pre-install transformers 5.x into .venv_t5/ ── + # Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing + # at runtime (slow, ~10-15s), we pre-install into a separate directory. + # The training subprocess just prepends .venv_t5/ to sys.path -- instant switch. + echo "" + echo " Pre-installing transformers 5.x for newer model support..." + mkdir -p "$VENV_T5_DIR" + run_quiet "install transformers 5.x" fast_install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0" + run_quiet "install huggingface_hub for t5" fast_install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.7.1" + run_quiet "install hf_xet for t5" fast_install --target "$VENV_T5_DIR" --no-deps "hf_xet==1.4.2" + # tiktoken is needed by Qwen-family tokenizers. Install with deps since + # regex/requests may be missing on Windows. + run_quiet "install tiktoken for t5" fast_install --target "$VENV_T5_DIR" "tiktoken" + echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/" +else + echo "✅ Python dependencies up to date — skipping" +fi + +# ── 7. Prefer prebuilt llama.cpp bundles before any source build path ── +UNSLOTH_HOME="$HOME/.unsloth" +mkdir -p "$UNSLOTH_HOME" +LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" +LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" +_NEED_LLAMA_SOURCE_BUILD=false +_LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" +_REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-latest}" +_HELPER_RELEASE_REPO="${UNSLOTH_LLAMA_RELEASE_REPO:-unslothai/llama.cpp}" +_RESOLVE_LLAMA_LOG="$(mktemp)" +set +e +python "$SCRIPT_DIR/install_llama_prebuilt.py" \ + --resolve-install-tag "$_REQUESTED_LLAMA_TAG" \ + --published-repo "$_HELPER_RELEASE_REPO" >"$_RESOLVE_LLAMA_LOG" 2>&1 +_RESOLVE_LLAMA_STATUS=$? +set -e +if [ "$_RESOLVE_LLAMA_STATUS" -eq 0 ]; then + _RESOLVED_LLAMA_TAG="$(tail -n 1 "$_RESOLVE_LLAMA_LOG" | tr -d '\r')" +else + _RESOLVED_LLAMA_TAG="" +fi +if [ -z "$_RESOLVED_LLAMA_TAG" ]; then + echo "" + echo "⚠️ Failed to resolve an installable prebuilt llama.cpp tag via $_HELPER_RELEASE_REPO" + cat "$_RESOLVE_LLAMA_LOG" >&2 || true + set +e + # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo + # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream + # bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp. + _RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" --resolve-llama-tag "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO" 2>/dev/null)" + _RESOLVE_UPSTREAM_STATUS=$? + set -e + if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then + if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then + # Try Unsloth release repo first, then fall back to ggml-org upstream + _RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${_HELPER_RELEASE_REPO}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" + if [ -z "$_RESOLVED_LLAMA_TAG" ]; then + _RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" + fi + fi + if [ -z "$_RESOLVED_LLAMA_TAG" ]; then + _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" + fi + fi + _NEED_LLAMA_SOURCE_BUILD=true + _SKIP_PREBUILT_INSTALL=true +fi +rm -f "$_RESOLVE_LLAMA_LOG" + echo "" -echo " Pre-installing transformers 5.x for newer model support..." -mkdir -p "$VENV_T5_DIR" -run_quiet "install transformers 5.x" fast_install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0" -run_quiet "install huggingface_hub for t5" fast_install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.7.1" -run_quiet "install hf_xet for t5" fast_install --target "$VENV_T5_DIR" --no-deps "hf_xet==1.4.2" -# tiktoken is needed by Qwen-family tokenizers. Install with deps since -# regex/requests may be missing on Windows. -run_quiet "install tiktoken for t5" fast_install --target "$VENV_T5_DIR" "tiktoken" -echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/" +echo "Resolved llama.cpp release tag: $_RESOLVED_LLAMA_TAG" -# ── 7. WSL: pre-install GGUF build dependencies ── +if [ "$_LLAMA_FORCE_COMPILE" = "1" ]; then + echo "" + echo "⚠️ UNSLOTH_LLAMA_FORCE_COMPILE=1 -- skipping prebuilt llama.cpp install" + _NEED_LLAMA_SOURCE_BUILD=true +else + echo "" + echo "Installing prebuilt llama.cpp bundle (preferred path)..." + if [ -d "$LLAMA_CPP_DIR" ]; then + echo "Existing llama.cpp install detected -- validating staged prebuilt update before replacement" + fi + if [ "${_SKIP_PREBUILT_INSTALL:-false}" = true ]; then + echo "⚠️ Skipping prebuilt install because prebuilt tag resolution failed -- falling back to source build" + else + _PREBUILT_CMD=( + python "$SCRIPT_DIR/install_llama_prebuilt.py" + --install-dir "$LLAMA_CPP_DIR" + --llama-tag "$_RESOLVED_LLAMA_TAG" + --published-repo "$_HELPER_RELEASE_REPO" + ) + if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then + _PREBUILT_CMD+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG") + fi + set +e + "${_PREBUILT_CMD[@]}" + _PREBUILT_STATUS=$? + set -e + + if [ "$_PREBUILT_STATUS" -eq 0 ]; then + echo "✅ Prebuilt llama.cpp installed and validated" + else + if [ -d "$LLAMA_CPP_DIR" ]; then + echo "⚠️ Prebuilt update failed; existing install was restored or cleaned before source build fallback" + fi + echo "⚠️ Prebuilt llama.cpp path unavailable or failed validation -- falling back to source build" + _NEED_LLAMA_SOURCE_BUILD=true + fi + fi +fi + +# ── 8. WSL: pre-install GGUF build dependencies for fallback source builds ── # On WSL, sudo requires a password and can't be entered during GGUF export # (runs in a non-interactive subprocess). Install build deps here instead. -if grep -qi microsoft /proc/version 2>/dev/null; then +if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && grep -qi microsoft /proc/version 2>/dev/null; then echo "" echo "⚠️ WSL detected -- installing build dependencies for GGUF export..." _GGUF_DEPS="pciutils build-essential cmake curl git libcurl4-openssl-dev" @@ -407,22 +523,19 @@ if grep -qi microsoft /proc/version 2>/dev/null; then fi fi -# ── 8. Build llama.cpp binaries for GGUF inference + export ── +# ── 9. Build llama.cpp binaries for GGUF inference + export when prebuilt install fails ── # Builds at ~/.unsloth/llama.cpp — a single shared location under the user's # home directory. This is used by both the inference server and the GGUF # export pipeline (unsloth-zoo). # - llama-server: for GGUF model inference # - llama-quantize: for GGUF export quantization (symlinked to root for check_llama_cpp()) -UNSLOTH_HOME="$HOME/.unsloth" -mkdir -p "$UNSLOTH_HOME" -LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" -LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" -if [ "${_SKIP_GGUF_BUILD:-}" = true ]; then +if [ "$_NEED_LLAMA_SOURCE_BUILD" = false ]; then + : +elif [ "${_SKIP_GGUF_BUILD:-}" = true ]; then echo "" echo "Skipping llama-server build (missing dependencies)" echo " Install the missing packages and re-run setup to enable GGUF inference." else -rm -rf "$LLAMA_CPP_DIR" { # Check prerequisites if ! command -v cmake &>/dev/null; then @@ -437,7 +550,13 @@ rm -rf "$LLAMA_CPP_DIR" echo "Building llama-server for GGUF inference..." BUILD_OK=true - run_quiet_no_exit "clone llama.cpp" git clone --depth 1 https://github.com/ggml-org/llama.cpp.git "$LLAMA_CPP_DIR" || BUILD_OK=false + _CLONE_BRANCH_ARGS=() + if [ "$_RESOLVED_LLAMA_TAG" != "latest" ] && [ -n "$_RESOLVED_LLAMA_TAG" ]; then + _CLONE_BRANCH_ARGS=(--branch "$_RESOLVED_LLAMA_TAG") + fi + _BUILD_TMP="${LLAMA_CPP_DIR}.build.$$" + rm -rf "$_BUILD_TMP" + run_quiet_no_exit "clone llama.cpp" git clone --depth 1 "${_CLONE_BRANCH_ARGS[@]}" https://github.com/ggml-org/llama.cpp.git "$_BUILD_TMP" || BUILD_OK=false if [ "$BUILD_OK" = true ]; then # Skip tests/examples we don't need (faster build) @@ -449,17 +568,40 @@ rm -rf "$LLAMA_CPP_DIR" echo " Using ccache for faster compilation" fi - # Detect CUDA: check nvcc on PATH, then common install locations + # Detect GPU backend: CUDA (NVIDIA) or ROCm (AMD) + GPU_BACKEND="" + + # Check for CUDA: check nvcc on PATH, then common install locations NVCC_PATH="" if command -v nvcc &>/dev/null; then NVCC_PATH="$(command -v nvcc)" + GPU_BACKEND="cuda" elif [ -x /usr/local/cuda/bin/nvcc ]; then NVCC_PATH="/usr/local/cuda/bin/nvcc" export PATH="/usr/local/cuda/bin:$PATH" + GPU_BACKEND="cuda" elif ls /usr/local/cuda-*/bin/nvcc &>/dev/null 2>&1; then # Pick the newest cuda-XX.X directory NVCC_PATH="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)" export PATH="$(dirname "$NVCC_PATH"):$PATH" + GPU_BACKEND="cuda" + fi + + # Check for ROCm (AMD) only if CUDA was not already selected + ROCM_HIPCC="" + if [ -z "$GPU_BACKEND" ]; then + if command -v hipcc &>/dev/null; then + ROCM_HIPCC="$(command -v hipcc)" + GPU_BACKEND="rocm" + elif [ -x /opt/rocm/bin/hipcc ]; then + ROCM_HIPCC="/opt/rocm/bin/hipcc" + export PATH="/opt/rocm/bin:$PATH" + GPU_BACKEND="rocm" + elif ls /opt/rocm-*/bin/hipcc &>/dev/null 2>&1; then + ROCM_HIPCC="$(ls -d /opt/rocm-*/bin/hipcc 2>/dev/null | sort -V | tail -1)" + export PATH="$(dirname "$ROCM_HIPCC"):$PATH" + GPU_BACKEND="rocm" + fi fi if [ -n "$NVCC_PATH" ]; then @@ -494,9 +636,53 @@ rm -rf "$LLAMA_CPP_DIR" # Multi-threaded nvcc compilation (uses all CPU cores per .cu file) CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_FLAGS=--threads=0" + elif [ "$GPU_BACKEND" = "rocm" ]; then + # Resolve hipcc symlinks to find the real ROCm root + _HIPCC_REAL="$(readlink -f "$ROCM_HIPCC" 2>/dev/null || printf '%s' "$ROCM_HIPCC")" + ROCM_ROOT="" + if command -v hipconfig &>/dev/null; then + ROCM_ROOT="$(hipconfig -R 2>/dev/null || true)" + fi + if [ -z "$ROCM_ROOT" ]; then + ROCM_ROOT="$(cd "$(dirname "$_HIPCC_REAL")/.." 2>/dev/null && pwd)" + fi + + echo " Building with ROCm support (AMD GPU, hipcc: $_HIPCC_REAL)..." + CMAKE_ARGS="$CMAKE_ARGS -DGGML_HIP=ON" + export ROCM_PATH="$ROCM_ROOT" + export HIP_PATH="$ROCM_ROOT" + + # Use upstream-recommended HIP compiler (not legacy hipcc-as-CXX) + if command -v hipconfig &>/dev/null; then + _HIP_CLANG_DIR="$(hipconfig -l 2>/dev/null || true)" + [ -n "$_HIP_CLANG_DIR" ] && export HIPCXX="$_HIP_CLANG_DIR/clang" + fi + + # Detect AMD GPU architecture (gfx target) + GPU_TARGETS="" + if command -v rocminfo &>/dev/null; then + _gfx_list=$(rocminfo 2>/dev/null | grep -oE 'gfx[0-9]{2,4}[a-z]?' | sort -u || true) + _valid_gfx="" + for _gfx in $_gfx_list; do + if [[ "$_gfx" =~ ^gfx[0-9]{2,4}[a-z]?$ ]]; then + _valid_gfx="${_valid_gfx}${_valid_gfx:+;}$_gfx" + fi + done + [ -n "$_valid_gfx" ] && GPU_TARGETS="$_valid_gfx" + fi + + if [ -n "$GPU_TARGETS" ]; then + echo " AMD GPU architectures: ${GPU_TARGETS//;/, } -- limiting build to detected targets" + CMAKE_ARGS="$CMAKE_ARGS -DGPU_TARGETS=${GPU_TARGETS}" + else + echo " Could not detect AMD GPU arch -- building for default targets (cmake will auto-detect)" + fi elif [ -d /usr/local/cuda ] || nvidia-smi &>/dev/null; then echo " CUDA driver detected but nvcc not found — building CPU-only" echo " To enable GPU: install cuda-toolkit or add nvcc to PATH" + elif [ -d /opt/rocm ] || command -v rocm-smi &>/dev/null; then + echo " ROCm driver detected but hipcc not found — building CPU-only" + echo " To enable GPU: install rocm-dev or add hipcc to PATH" else echo " Building CPU-only (no CUDA detected)..." fi @@ -509,21 +695,29 @@ rm -rf "$LLAMA_CPP_DIR" CMAKE_GENERATOR_ARGS="-G Ninja" fi - run_quiet_no_exit "cmake llama.cpp" cmake $CMAKE_GENERATOR_ARGS -S "$LLAMA_CPP_DIR" -B "$LLAMA_CPP_DIR/build" $CMAKE_ARGS || BUILD_OK=false + run_quiet_no_exit "cmake llama.cpp" cmake $CMAKE_GENERATOR_ARGS -S "$_BUILD_TMP" -B "$_BUILD_TMP/build" $CMAKE_ARGS || BUILD_OK=false fi if [ "$BUILD_OK" = true ]; then - run_quiet_no_exit "build llama-server" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false + run_quiet_no_exit "build llama-server" cmake --build "$_BUILD_TMP/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false fi # Also build llama-quantize (needed by unsloth-zoo's GGUF export pipeline) if [ "$BUILD_OK" = true ]; then - run_quiet_no_exit "build llama-quantize" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-quantize -j"$NCPU" || true - # Symlink to llama.cpp root — check_llama_cpp() looks for the binary there + run_quiet_no_exit "build llama-quantize" cmake --build "$_BUILD_TMP/build" --config Release --target llama-quantize -j"$NCPU" || true + fi + + # Swap only after build succeeds -- preserves existing install on failure + if [ "$BUILD_OK" = true ]; then + rm -rf "$LLAMA_CPP_DIR" + mv "$_BUILD_TMP" "$LLAMA_CPP_DIR" + # Symlink to llama.cpp root -- check_llama_cpp() looks for the binary there QUANTIZE_BIN="$LLAMA_CPP_DIR/build/bin/llama-quantize" if [ -f "$QUANTIZE_BIN" ]; then ln -sf build/bin/llama-quantize "$LLAMA_CPP_DIR/llama-quantize" fi + else + rm -rf "$_BUILD_TMP" fi if [ "$BUILD_OK" = true ]; then @@ -543,9 +737,15 @@ rm -rf "$LLAMA_CPP_DIR" fi # end _SKIP_GGUF_BUILD check echo "" +if [ "${SKIP_STUDIO_BASE:-0}" = "1" ]; then + _DONE_LINE="║ Setup Complete! ║" +else + _DONE_LINE="║ Update Complete! ║" +fi + if [ "$IS_COLAB" = true ]; then echo "╔══════════════════════════════════════╗" - echo "║ Setup Complete! ║" + echo "$_DONE_LINE" echo "╠══════════════════════════════════════╣" echo "║ Unsloth Studio is ready to start ║" echo "║ in your Colab notebook! ║" @@ -555,7 +755,7 @@ if [ "$IS_COLAB" = true ]; then echo "╚══════════════════════════════════════╝" else echo "╔══════════════════════════════════════╗" - echo "║ Setup Complete! ║" + echo "$_DONE_LINE" echo "╠══════════════════════════════════════╣" echo "║ Launch with: ║" echo "║ ║" diff --git a/tests/python/__init__.py b/tests/python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/python/test_cross_platform_parity.py b/tests/python/test_cross_platform_parity.py new file mode 100644 index 0000000000..6dd41be9fa --- /dev/null +++ b/tests/python/test_cross_platform_parity.py @@ -0,0 +1,137 @@ +"""Cross-platform parity tests between install.sh and install.ps1.""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +INSTALL_SH = REPO_ROOT / "install.sh" +INSTALL_PS1 = REPO_ROOT / "install.ps1" + + +class TestNoTorchBackendAutoInInstallSh: + """install.sh primary install paths must not use --torch-backend=auto. + + The fallback else-branch (when TORCH_INDEX_URL is empty) is allowed to + use --torch-backend=auto since that is the last-resort recovery path. + """ + + def test_no_torch_backend_auto_outside_fallback(self): + lines = INSTALL_SH.read_text().splitlines() + # Find the fallback block: starts with the "else" after the + # TORCH_INDEX_URL check and ends at the next "fi". + fallback_start = None + fallback_end = None + for i, line in enumerate(lines): + if fallback_start is None and "GPU detection failed" in line: + fallback_start = i + elif ( + fallback_start is not None + and fallback_end is None + and line.strip() == "fi" + ): + fallback_end = i + break + fallback_range = ( + range(fallback_start or 0, (fallback_end or 0) + 1) + if fallback_start + else range(0) + ) + + matches = [ + (i + 1, line) + for i, line in enumerate(lines) + if "--torch-backend=auto" in line + and not line.lstrip().startswith("#") + and i not in fallback_range + ] + assert matches == [], ( + f"install.sh contains --torch-backend=auto outside the fallback block at lines: " + f"{[m[0] for m in matches]}" + ) + + def test_fallback_uses_torch_backend_auto(self): + """The fallback branch should use --torch-backend=auto as recovery.""" + text = INSTALL_SH.read_text() + assert ( + "GPU detection failed" in text + ), "install.sh should have a fallback branch for when GPU detection fails" + + +class TestInstallShHasGpuDetection: + """install.sh must contain the get_torch_index_url function.""" + + def test_function_exists(self): + text = INSTALL_SH.read_text() + assert ( + "get_torch_index_url()" in text + ), "install.sh is missing the get_torch_index_url() function" + + def test_torch_index_url_assigned(self): + text = INSTALL_SH.read_text() + assert ( + "TORCH_INDEX_URL=$(get_torch_index_url)" in text + ), "install.sh should assign TORCH_INDEX_URL from get_torch_index_url()" + + +class TestCudaMappingParity: + """CUDA version thresholds must match between install.sh and install.ps1.""" + + @staticmethod + def _extract_cuda_thresholds_sh(text: str) -> list[str]: + """Extract cu* suffixes from the major/minor comparison chain in install.sh.""" + # Only match lines in the if/elif chain that compare _major/_minor + in_func = False + results = [] + for line in text.splitlines(): + if "get_torch_index_url()" in line: + in_func = True + continue + if in_func and line.startswith("}"): + break + if in_func and ("_major" in line or "_minor" in line): + m = re.search(r"/(cu\d+|cpu)", line) + if m: + results.append(m.group(1)) + return results + + @staticmethod + def _extract_cuda_thresholds_ps1(text: str) -> list[str]: + """Extract cu* suffixes from the major/minor comparison chain in install.ps1.""" + in_func = False + depth = 0 + results = [] + for line in text.splitlines(): + if "function Get-TorchIndexUrl" in line: + in_func = True + depth = 1 + continue + if in_func: + depth += line.count("{") - line.count("}") + if depth <= 0: + break + # Only match the if-chain lines that compare $major/$minor + if "$major" in line or "$minor" in line: + m = re.search(r"/(cu\d+|cpu)", line) + if m: + results.append(m.group(1)) + return results + + def test_same_cuda_suffixes(self): + """Both scripts should produce the same ordered list of CUDA index suffixes.""" + sh_text = INSTALL_SH.read_text() + ps1_text = INSTALL_PS1.read_text() + + sh_thresholds = self._extract_cuda_thresholds_sh(sh_text) + ps1_thresholds = self._extract_cuda_thresholds_ps1(ps1_text) + + assert len(sh_thresholds) > 0, "Could not extract thresholds from install.sh" + assert len(ps1_thresholds) > 0, "Could not extract thresholds from install.ps1" + assert sh_thresholds == ps1_thresholds, ( + f"CUDA mapping mismatch:\n" + f" install.sh: {sh_thresholds}\n" + f" install.ps1: {ps1_thresholds}" + ) diff --git a/tests/python/test_install_python_stack.py b/tests/python/test_install_python_stack.py new file mode 100644 index 0000000000..16538ae42b --- /dev/null +++ b/tests/python/test_install_python_stack.py @@ -0,0 +1,56 @@ +"""Tests for install_python_stack._build_uv_cmd torch-backend handling.""" + +from __future__ import annotations + +import importlib +import os +import sys +from pathlib import Path +from unittest import mock + +import pytest + +# Add the studio directory so we can import install_python_stack +STUDIO_DIR = Path(__file__).resolve().parents[2] / "studio" +sys.path.insert(0, str(STUDIO_DIR)) + +# _build_uv_cmd lives at module level; import after path setup. +# We need to mock parts of the module that do work at import time. +import install_python_stack as ips + + +class TestBuildUvCmdTorchBackend: + """Verify _build_uv_cmd only adds --torch-backend when UV_TORCH_BACKEND is set.""" + + def _call(self, args: tuple[str, ...] = ()) -> list[str]: + return ips._build_uv_cmd(args) + + def test_default_no_torch_backend(self): + """Without UV_TORCH_BACKEND env var, no --torch-backend flag.""" + env = os.environ.copy() + env.pop("UV_TORCH_BACKEND", None) + with mock.patch.dict(os.environ, env, clear = True): + cmd = self._call(("somepackage",)) + assert not any( + a.startswith("--torch-backend") for a in cmd + ), f"--torch-backend should not appear by default, got: {cmd}" + + def test_uv_torch_backend_auto(self): + """UV_TORCH_BACKEND=auto adds --torch-backend=auto.""" + with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "auto"}): + cmd = self._call(("somepackage",)) + assert "--torch-backend=auto" in cmd + + def test_uv_torch_backend_cpu(self): + """UV_TORCH_BACKEND=cpu adds --torch-backend=cpu.""" + with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}): + cmd = self._call(("somepackage",)) + assert "--torch-backend=cpu" in cmd + + def test_uv_torch_backend_empty(self): + """UV_TORCH_BACKEND="" (empty string) should NOT add --torch-backend.""" + with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": ""}): + cmd = self._call(("somepackage",)) + assert not any( + a.startswith("--torch-backend") for a in cmd + ), f"Empty UV_TORCH_BACKEND should not add flag, got: {cmd}" diff --git a/tests/run_all.sh b/tests/run_all.sh new file mode 100755 index 0000000000..d7fdb38e74 --- /dev/null +++ b/tests/run_all.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Run all installer tests. +set -e + +TESTS_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "=== Bash tests ===" +sh "$TESTS_DIR/sh/test_get_torch_index_url.sh" + +echo "" +echo "=== Python tests ===" +python -m pytest "$TESTS_DIR/python/test_install_python_stack.py" -v +python -m pytest "$TESTS_DIR/python/test_cross_platform_parity.py" -v + +echo "" +echo "All tests passed." diff --git a/tests/sh/test_get_torch_index_url.sh b/tests/sh/test_get_torch_index_url.sh new file mode 100755 index 0000000000..6387922712 --- /dev/null +++ b/tests/sh/test_get_torch_index_url.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Unit tests for get_torch_index_url() from install.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INSTALL_SH="$SCRIPT_DIR/../../install.sh" +PASS=0 +FAIL=0 + +# Extract only the get_torch_index_url function from install.sh +# Also replace the hardcoded /usr/bin/nvidia-smi fallback with a +# controllable path so we can test the "no GPU" scenario on GPU machines. +_FUNC_FILE=$(mktemp) +_FAKE_SMI_DIR=$(mktemp -d) +sed -n '/^get_torch_index_url()/,/^}/p' "$INSTALL_SH" \ + | sed "s|/usr/bin/nvidia-smi|$_FAKE_SMI_DIR/nvidia-smi-absent|g" \ + > "$_FUNC_FILE" + +# Save system PATH so we always have basic tools (uname, grep, head, etc.) +_SYS_PATH="/usr/local/bin:/usr/bin:/bin" + +assert_eq() { + _label="$1"; _expected="$2"; _actual="$3" + if [ "$_actual" = "$_expected" ]; then + echo " PASS: $_label" + PASS=$((PASS + 1)) + else + echo " FAIL: $_label (expected '$_expected', got '$_actual')" + FAIL=$((FAIL + 1)) + fi +} + +# Helper: create a mock nvidia-smi that prints a given CUDA version string +make_mock_smi() { + _dir=$(mktemp -d) + cat > "$_dir/nvidia-smi" </dev/null || true) + [ -n "$_real" ] && ln -sf "$_real" "$_TOOLS_DIR/$_cmd" +done + +# Helper: run get_torch_index_url with a custom PATH +# $1 = directory with mock nvidia-smi (prepended to PATH), or "none" for no-GPU test +run_func() { + _mock_dir="$1" + if [ "$_mock_dir" = "none" ]; then + # Minimal PATH with only basic tools, no nvidia-smi anywhere + PATH="$_TOOLS_DIR" bash -c ". '$_FUNC_FILE'; get_torch_index_url" 2>/dev/null + else + # Put mock nvidia-smi dir first, then basic tools + PATH="$_mock_dir:$_TOOLS_DIR" bash -c ". '$_FUNC_FILE'; get_torch_index_url" 2>/dev/null + fi +} + +echo "=== test_get_torch_index_url ===" + +# 1) No nvidia-smi available -> cpu +_result=$(run_func "none") +assert_eq "no nvidia-smi -> cpu" "https://download.pytorch.org/whl/cpu" "$_result" + +# 2) CUDA 12.6 -> cu126 +_dir=$(make_mock_smi "12.6") +_result=$(run_func "$_dir") +assert_eq "CUDA 12.6 -> cu126" "https://download.pytorch.org/whl/cu126" "$_result" +rm -rf "$_dir" + +# 3) CUDA 12.8 -> cu128 +_dir=$(make_mock_smi "12.8") +_result=$(run_func "$_dir") +assert_eq "CUDA 12.8 -> cu128" "https://download.pytorch.org/whl/cu128" "$_result" +rm -rf "$_dir" + +# 4) CUDA 13.0 -> cu130 +_dir=$(make_mock_smi "13.0") +_result=$(run_func "$_dir") +assert_eq "CUDA 13.0 -> cu130" "https://download.pytorch.org/whl/cu130" "$_result" +rm -rf "$_dir" + +# 5) CUDA 12.4 -> cu124 +_dir=$(make_mock_smi "12.4") +_result=$(run_func "$_dir") +assert_eq "CUDA 12.4 -> cu124" "https://download.pytorch.org/whl/cu124" "$_result" +rm -rf "$_dir" + +# 6) CUDA 11.8 -> cu118 +_dir=$(make_mock_smi "11.8") +_result=$(run_func "$_dir") +assert_eq "CUDA 11.8 -> cu118" "https://download.pytorch.org/whl/cu118" "$_result" +rm -rf "$_dir" + +# 7) CUDA 10.2 (too old) -> cpu +_dir=$(make_mock_smi "10.2") +_result=$(run_func "$_dir") +assert_eq "CUDA 10.2 -> cpu" "https://download.pytorch.org/whl/cpu" "$_result" +rm -rf "$_dir" + +# 8) Unparseable nvidia-smi output -> cu126 default +_dir=$(mktemp -d) +cat > "$_dir/nvidia-smi" <<'MOCK' +#!/bin/sh +echo "something completely unexpected" +MOCK +chmod +x "$_dir/nvidia-smi" +_result=$(run_func "$_dir") +assert_eq "unparseable -> cu126" "https://download.pytorch.org/whl/cu126" "$_result" +rm -rf "$_dir" + +rm -f "$_FUNC_FILE" +rm -rf "$_FAKE_SMI_DIR" +rm -rf "$_TOOLS_DIR" + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] || exit 1 diff --git a/tests/studio/install/smoke_test_llama_prebuilt.py b/tests/studio/install/smoke_test_llama_prebuilt.py new file mode 100644 index 0000000000..994757d2e2 --- /dev/null +++ b/tests/studio/install/smoke_test_llama_prebuilt.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import importlib.util +import shutil +import sys +import tempfile +import time +from pathlib import Path + + +PACKAGE_ROOT = Path(__file__).resolve().parents[3] +INSTALLER_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py" + + +def load_installer_module(): + spec = importlib.util.spec_from_file_location( + "studio_install_llama_prebuilt", INSTALLER_PATH + ) + if spec is None or spec.loader is None: + raise RuntimeError(f"unable to load installer module from {INSTALLER_PATH}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +installer = load_installer_module() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description = ( + "Run a real end-to-end prebuilt llama.cpp install into an isolated temporary " + "directory on the current machine." + ) + ) + parser.add_argument( + "--llama-tag", + default = "latest", + help = "llama.cpp tag to resolve. Defaults to the approved prebuilt tag for this host.", + ) + parser.add_argument( + "--published-repo", + default = installer.DEFAULT_PUBLISHED_REPO, + help = "Published bundle repository used for Linux CUDA selection.", + ) + parser.add_argument( + "--published-release-tag", + default = installer.DEFAULT_PUBLISHED_TAG or "", + help = "Optional published GitHub release tag to pin.", + ) + parser.add_argument( + "--work-dir", + default = "", + help = ( + "Optional directory under which the smoke install temp dir will be created. " + "If omitted, defaults to ./.tmp/llama-prebuilt-smoke under the current directory." + ), + ) + parser.add_argument( + "--keep-temp", + action = "store_true", + help = "Keep the temporary smoke install directory after success.", + ) + return parser.parse_args() + + +def smoke_root_base(work_dir: str) -> Path: + if work_dir: + return Path(work_dir).expanduser().resolve() + return (Path.cwd() / ".tmp" / "llama-prebuilt-smoke").resolve() + + +def make_smoke_root(base_dir: Path) -> Path: + base_dir.mkdir(parents = True, exist_ok = True) + timestamp = time.strftime("%Y%m%d%H%M%S", time.gmtime()) + return Path(tempfile.mkdtemp(prefix = f"run-{timestamp}-", dir = base_dir)) + + +def main() -> int: + args = parse_args() + host = installer.detect_host() + smoke_base = smoke_root_base(args.work_dir) + smoke_root = make_smoke_root(smoke_base) + install_dir = smoke_root / "install" / "llama.cpp" + choice = None + + print(f"[smoke] host={host.system} machine={host.machine}") + print(f"[smoke] temp_root={smoke_root}") + + try: + requested_tag, resolved_tag, attempts, _approved_checksums = ( + installer.resolve_install_attempts( + args.llama_tag, + host, + args.published_repo, + args.published_release_tag, + ) + ) + choice = attempts[0] + print(f"[smoke] requested_tag={requested_tag}") + print(f"[smoke] resolved_tag={resolved_tag}") + print(f"[smoke] selected_asset={choice.name}") + print(f"[smoke] selected_source={choice.source_label}") + print(f"[smoke] install_dir={install_dir}") + installer.install_prebuilt( + install_dir = install_dir, + llama_tag = args.llama_tag, + published_repo = args.published_repo, + published_release_tag = args.published_release_tag, + ) + print(f"[smoke] PASS install_dir={install_dir}") + print( + "[smoke] note=This was a real prebuilt install into an isolated temp directory." + ) + return installer.EXIT_SUCCESS + except SystemExit as exc: + code = int(exc.code) if isinstance(exc.code, int) else installer.EXIT_ERROR + if code == installer.EXIT_FALLBACK: + print(f"[smoke] FALLBACK install_dir={install_dir}") + print( + "[smoke] note=Prebuilt path failed and would fall back to source build in setup." + ) + print(installer.collect_system_report(host, choice, install_dir)) + else: + print(f"[smoke] ERROR exit_code={code} install_dir={install_dir}") + return code + except Exception as exc: + print(f"[smoke] ERROR {exc}") + print(installer.collect_system_report(host, choice, install_dir)) + return installer.EXIT_ERROR + finally: + if args.keep_temp: + print(f"[smoke] keeping_temp_root={smoke_root}") + elif smoke_root.exists(): + shutil.rmtree(smoke_root, ignore_errors = True) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/studio/install/test_install_llama_prebuilt_logic.py b/tests/studio/install/test_install_llama_prebuilt_logic.py new file mode 100644 index 0000000000..eb30ac2745 --- /dev/null +++ b/tests/studio/install/test_install_llama_prebuilt_logic.py @@ -0,0 +1,630 @@ +import importlib.util +import io +import json +import os +import sys +import tarfile +import zipfile +from pathlib import Path + +import pytest + + +PACKAGE_ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py" +SPEC = importlib.util.spec_from_file_location( + "studio_install_llama_prebuilt", MODULE_PATH +) +assert SPEC is not None and SPEC.loader is not None +INSTALL_LLAMA_PREBUILT = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = INSTALL_LLAMA_PREBUILT +SPEC.loader.exec_module(INSTALL_LLAMA_PREBUILT) + +PrebuiltFallback = INSTALL_LLAMA_PREBUILT.PrebuiltFallback +extract_archive = INSTALL_LLAMA_PREBUILT.extract_archive +binary_env = INSTALL_LLAMA_PREBUILT.binary_env +HostInfo = INSTALL_LLAMA_PREBUILT.HostInfo +AssetChoice = INSTALL_LLAMA_PREBUILT.AssetChoice +ApprovedArtifactHash = INSTALL_LLAMA_PREBUILT.ApprovedArtifactHash +ApprovedReleaseChecksums = INSTALL_LLAMA_PREBUILT.ApprovedReleaseChecksums +hydrate_source_tree = INSTALL_LLAMA_PREBUILT.hydrate_source_tree +validate_prebuilt_choice = INSTALL_LLAMA_PREBUILT.validate_prebuilt_choice +activate_install_tree = INSTALL_LLAMA_PREBUILT.activate_install_tree +create_install_staging_dir = INSTALL_LLAMA_PREBUILT.create_install_staging_dir +sha256_file = INSTALL_LLAMA_PREBUILT.sha256_file +source_archive_logical_name = INSTALL_LLAMA_PREBUILT.source_archive_logical_name + + +def approved_checksums_for( + upstream_tag: str, *, source_archive: Path, bundle_archive: Path, bundle_name: str +) -> ApprovedReleaseChecksums: + return ApprovedReleaseChecksums( + repo = "local", + release_tag = upstream_tag, + upstream_tag = upstream_tag, + source_commit = None, + artifacts = { + source_archive_logical_name(upstream_tag): ApprovedArtifactHash( + asset_name = source_archive_logical_name(upstream_tag), + sha256 = sha256_file(source_archive), + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + bundle_name: ApprovedArtifactHash( + asset_name = bundle_name, + sha256 = sha256_file(bundle_archive), + repo = "local", + kind = "local-test-bundle", + ), + }, + ) + + +def test_extract_archive_allows_safe_tar_symlink_chain(tmp_path: Path): + archive_path = tmp_path / "bundle.tar.gz" + payload = b"shared-object" + + with tarfile.open(archive_path, "w:gz") as archive: + versioned = tarfile.TarInfo("libllama.so.0.0.1") + versioned.size = len(payload) + archive.addfile(versioned, io_bytes(payload)) + + soname = tarfile.TarInfo("libllama.so.0") + soname.type = tarfile.SYMTYPE + soname.linkname = "libllama.so.0.0.1" + archive.addfile(soname) + + linker_name = tarfile.TarInfo("libllama.so") + linker_name.type = tarfile.SYMTYPE + linker_name.linkname = "libllama.so.0" + archive.addfile(linker_name) + + destination = tmp_path / "extract" + extract_archive(archive_path, destination) + + assert (destination / "libllama.so.0.0.1").read_bytes() == payload + assert (destination / "libllama.so.0").is_symlink() + assert (destination / "libllama.so").is_symlink() + assert (destination / "libllama.so").resolve().read_bytes() == payload + + +def test_extract_archive_allows_safe_tar_hardlink(tmp_path: Path): + archive_path = tmp_path / "bundle.tar.gz" + payload = b"quantize" + + with tarfile.open(archive_path, "w:gz") as archive: + target = tarfile.TarInfo("llama-quantize") + target.size = len(payload) + archive.addfile(target, io_bytes(payload)) + + hardlink = tarfile.TarInfo("llama-quantize-copy") + hardlink.type = tarfile.LNKTYPE + hardlink.linkname = "llama-quantize" + archive.addfile(hardlink) + + destination = tmp_path / "extract" + extract_archive(archive_path, destination) + + assert (destination / "llama-quantize-copy").read_bytes() == payload + assert not (destination / "llama-quantize-copy").is_symlink() + + +def test_extract_archive_rejects_absolute_tar_symlink_target(tmp_path: Path): + archive_path = tmp_path / "bundle.tar.gz" + + with tarfile.open(archive_path, "w:gz") as archive: + entry = tarfile.TarInfo("libllama.so") + entry.type = tarfile.SYMTYPE + entry.linkname = "/tmp/libllama.so.0" + archive.addfile(entry) + + with pytest.raises(PrebuiltFallback, match = "archive link used an absolute target"): + extract_archive(archive_path, tmp_path / "extract") + + +def test_extract_archive_rejects_escaping_tar_symlink_target(tmp_path: Path): + archive_path = tmp_path / "bundle.tar.gz" + + with tarfile.open(archive_path, "w:gz") as archive: + entry = tarfile.TarInfo("libllama.so") + entry.type = tarfile.SYMTYPE + entry.linkname = "../outside/libllama.so.0" + archive.addfile(entry) + + with pytest.raises(PrebuiltFallback, match = "archive link escaped destination"): + extract_archive(archive_path, tmp_path / "extract") + + +def test_extract_archive_rejects_unresolved_tar_symlink_target(tmp_path: Path): + archive_path = tmp_path / "bundle.tar.gz" + + with tarfile.open(archive_path, "w:gz") as archive: + entry = tarfile.TarInfo("libllama.so") + entry.type = tarfile.SYMTYPE + entry.linkname = "libllama.so.0" + archive.addfile(entry) + + with pytest.raises(PrebuiltFallback, match = "unresolved link entries"): + extract_archive(archive_path, tmp_path / "extract") + + +def test_extract_archive_rejects_zip_symlink_entry(tmp_path: Path): + archive_path = tmp_path / "bundle.zip" + + with zipfile.ZipFile(archive_path, "w") as archive: + info = zipfile.ZipInfo("libllama.so") + info.create_system = 3 + info.external_attr = 0o120777 << 16 + archive.writestr(info, "libllama.so.0") + + with pytest.raises(PrebuiltFallback, match = "zip archive contained a symlink entry"): + extract_archive(archive_path, tmp_path / "extract") + + +def test_hydrate_source_tree_extracts_upstream_archive_contents( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + upstream_tag = "b9999" + archive_path = tmp_path / "llama.cpp-source.tar.gz" + with tarfile.open(archive_path, "w:gz") as archive: + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/CMakeLists.txt", + b"cmake_minimum_required(VERSION 3.14)\n", + ) + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/convert_hf_to_gguf.py", + b"#!/usr/bin/env python3\nimport gguf\n", + ) + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/gguf-py/gguf/__init__.py", + b"__all__ = []\n", + ) + + source_urls = set(INSTALL_LLAMA_PREBUILT.upstream_source_archive_urls(upstream_tag)) + + def fake_download_file(url: str, destination: Path) -> None: + assert url in source_urls + destination.write_bytes(archive_path.read_bytes()) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "download_file", fake_download_file) + + install_dir = tmp_path / "install" + work_dir = tmp_path / "work" + work_dir.mkdir() + hydrate_source_tree( + upstream_tag, install_dir, work_dir, expected_sha256 = sha256_file(archive_path) + ) + + assert (install_dir / "CMakeLists.txt").exists() + assert (install_dir / "convert_hf_to_gguf.py").exists() + assert (install_dir / "gguf-py" / "gguf" / "__init__.py").exists() + assert not (install_dir / f"llama.cpp-{upstream_tag}").exists() + + +def test_validate_prebuilt_choice_creates_repo_shaped_linux_install( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + upstream_tag = "b9998" + bundle_name = "app-b9998-linux-x64-cuda13-newer.tar.gz" + source_archive = tmp_path / "source.tar.gz" + bundle_archive = tmp_path / "bundle.tar.gz" + with tarfile.open(source_archive, "w:gz") as archive: + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/CMakeLists.txt", + b"cmake_minimum_required(VERSION 3.14)\n", + ) + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/convert_hf_to_gguf.py", + b"#!/usr/bin/env python3\nimport gguf\n", + ) + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/gguf-py/gguf/__init__.py", + b"__all__ = []\n", + ) + with tarfile.open(bundle_archive, "w:gz") as archive: + add_bytes_to_tar(archive, "llama-server", b"#!/bin/sh\nexit 0\n", mode = 0o755) + add_bytes_to_tar(archive, "llama-quantize", b"#!/bin/sh\nexit 0\n", mode = 0o755) + add_bytes_to_tar(archive, "libllama.so.0.0.1", b"libllama") + add_symlink_to_tar(archive, "libllama.so.0", "libllama.so.0.0.1") + add_symlink_to_tar(archive, "libllama.so", "libllama.so.0") + add_bytes_to_tar(archive, "libggml.so.0.9.8", b"libggml") + add_symlink_to_tar(archive, "libggml.so.0", "libggml.so.0.9.8") + add_symlink_to_tar(archive, "libggml.so", "libggml.so.0") + add_bytes_to_tar(archive, "libggml-base.so.0.9.8", b"libggml-base") + add_symlink_to_tar(archive, "libggml-base.so.0", "libggml-base.so.0.9.8") + add_symlink_to_tar(archive, "libggml-base.so", "libggml-base.so.0") + add_bytes_to_tar(archive, "libggml-cpu-x64.so.0.9.8", b"libggml-cpu") + add_symlink_to_tar(archive, "libggml-cpu-x64.so.0", "libggml-cpu-x64.so.0.9.8") + add_symlink_to_tar(archive, "libggml-cpu-x64.so", "libggml-cpu-x64.so.0") + add_bytes_to_tar(archive, "libmtmd.so.0.0.1", b"libmtmd") + add_symlink_to_tar(archive, "libmtmd.so.0", "libmtmd.so.0.0.1") + add_symlink_to_tar(archive, "libmtmd.so", "libmtmd.so.0") + add_bytes_to_tar(archive, "BUILD_INFO.txt", b"bundle metadata\n") + add_bytes_to_tar(archive, "THIRD_PARTY_LICENSES.txt", b"licenses\n") + + source_urls = set(INSTALL_LLAMA_PREBUILT.upstream_source_archive_urls(upstream_tag)) + + def fake_download_file(url: str, destination: Path) -> None: + if url in source_urls: + destination.write_bytes(source_archive.read_bytes()) + return + if url == "file://bundle": + destination.write_bytes(bundle_archive.read_bytes()) + return + raise AssertionError(f"unexpected download url: {url}") + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "download_file", fake_download_file) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_bytes", + lambda url, **_: b"#!/usr/bin/env python3\nimport gguf\n", + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "preflight_linux_installed_binaries", + lambda *args, **kwargs: None, + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, "validate_quantize", lambda *args, **kwargs: None + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, "validate_server", lambda *args, **kwargs: None + ) + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "local", + tag = upstream_tag, + name = bundle_name, + url = "file://bundle", + source_label = "local", + is_ready_bundle = True, + install_kind = "linux-cuda", + bundle_profile = "cuda13-newer", + runtime_line = "cuda13", + expected_sha256 = sha256_file(bundle_archive), + ) + + install_dir = tmp_path / "install" + work_dir = tmp_path / "work" + work_dir.mkdir() + probe_path = tmp_path / "stories260K.gguf" + quantized_path = tmp_path / "stories260K-q4.gguf" + validate_prebuilt_choice( + choice, + host, + install_dir, + work_dir, + probe_path, + requested_tag = upstream_tag, + llama_tag = upstream_tag, + approved_checksums = approved_checksums_for( + upstream_tag, + source_archive = source_archive, + bundle_archive = bundle_archive, + bundle_name = bundle_name, + ), + prebuilt_fallback_used = False, + quantized_path = quantized_path, + ) + + assert (install_dir / "gguf-py" / "gguf" / "__init__.py").exists() + assert (install_dir / "convert_hf_to_gguf.py").exists() + assert (install_dir / "build" / "bin" / "llama-server").exists() + assert (install_dir / "build" / "bin" / "llama-quantize").exists() + assert (install_dir / "build" / "bin" / "libllama.so").exists() + assert (install_dir / "llama-server").exists() + assert (install_dir / "llama-quantize").exists() + assert (install_dir / "UNSLOTH_PREBUILT_INFO.json").exists() + assert (install_dir / "BUILD_INFO.txt").exists() + + +def test_validate_prebuilt_choice_creates_repo_shaped_windows_install( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + upstream_tag = "b9997" + bundle_name = "app-b9997-windows-x64-cpu.zip" + source_archive = tmp_path / "source.tar.gz" + bundle_archive = tmp_path / "bundle.zip" + with tarfile.open(source_archive, "w:gz") as archive: + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/CMakeLists.txt", + b"cmake_minimum_required(VERSION 3.14)\n", + ) + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/convert_hf_to_gguf.py", + b"#!/usr/bin/env python3\nimport gguf\n", + ) + add_bytes_to_tar( + archive, + f"llama.cpp-{upstream_tag}/gguf-py/gguf/__init__.py", + b"__all__ = []\n", + ) + with zipfile.ZipFile(bundle_archive, "w") as archive: + archive.writestr("llama-server.exe", b"MZ") + archive.writestr("llama-quantize.exe", b"MZ") + archive.writestr("llama.dll", b"DLL") + archive.writestr("BUILD_INFO.txt", b"bundle metadata\n") + + source_urls = set(INSTALL_LLAMA_PREBUILT.upstream_source_archive_urls(upstream_tag)) + + def fake_download_file(url: str, destination: Path) -> None: + if url in source_urls: + destination.write_bytes(source_archive.read_bytes()) + return + if url == "file://bundle.zip": + destination.write_bytes(bundle_archive.read_bytes()) + return + raise AssertionError(f"unexpected download url: {url}") + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "download_file", fake_download_file) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_bytes", + lambda url, **_: b"#!/usr/bin/env python3\nimport gguf\n", + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "preflight_linux_installed_binaries", + lambda *args, **kwargs: None, + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, "validate_quantize", lambda *args, **kwargs: None + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, "validate_server", lambda *args, **kwargs: None + ) + + host = HostInfo( + system = "Windows", + machine = "AMD64", + is_windows = True, + is_linux = False, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "local", + tag = upstream_tag, + name = bundle_name, + url = "file://bundle.zip", + source_label = "local", + is_ready_bundle = True, + install_kind = "windows-cpu", + expected_sha256 = sha256_file(bundle_archive), + ) + + install_dir = tmp_path / "install" + work_dir = tmp_path / "work" + work_dir.mkdir() + probe_path = tmp_path / "stories260K.gguf" + quantized_path = tmp_path / "stories260K-q4.gguf" + validate_prebuilt_choice( + choice, + host, + install_dir, + work_dir, + probe_path, + requested_tag = upstream_tag, + llama_tag = upstream_tag, + approved_checksums = approved_checksums_for( + upstream_tag, + source_archive = source_archive, + bundle_archive = bundle_archive, + bundle_name = bundle_name, + ), + prebuilt_fallback_used = False, + quantized_path = quantized_path, + ) + + assert (install_dir / "gguf-py" / "gguf" / "__init__.py").exists() + assert (install_dir / "convert_hf_to_gguf.py").exists() + assert (install_dir / "build" / "bin" / "Release" / "llama-server.exe").exists() + assert (install_dir / "build" / "bin" / "Release" / "llama-quantize.exe").exists() + assert (install_dir / "build" / "bin" / "Release" / "llama.dll").exists() + assert not (install_dir / "llama-server.exe").exists() + assert (install_dir / "UNSLOTH_PREBUILT_INFO.json").exists() + assert (install_dir / "BUILD_INFO.txt").exists() + + +def test_activate_install_tree_restores_existing_install_after_activation_failure( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + (install_dir / "old.txt").write_text("old install\n") + + staging_dir = create_install_staging_dir(install_dir) + (staging_dir / "new.txt").write_text("new install\n") + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "confirm_install_tree", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + RuntimeError("activation confirm failed") + ), + ) + + with pytest.raises( + PrebuiltFallback, + match = "activation failed; restored previous install", + ): + activate_install_tree(staging_dir, install_dir, host) + + assert (install_dir / "old.txt").read_text() == "old install\n" + assert not (install_dir / "new.txt").exists() + assert not staging_dir.exists() + assert not (tmp_path / ".staging").exists() + + output = capsys.readouterr().out + assert "moving existing install to rollback path" in output + assert "restored previous install from rollback path" in output + + +def test_activate_install_tree_cleans_all_paths_when_rollback_restore_fails( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + (install_dir / "old.txt").write_text("old install\n") + + staging_dir = create_install_staging_dir(install_dir) + (staging_dir / "new.txt").write_text("new install\n") + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "confirm_install_tree", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + RuntimeError("activation confirm failed") + ), + ) + + original_replace = INSTALL_LLAMA_PREBUILT.os.replace + + def flaky_replace(src, dst): + src_path = Path(src) + dst_path = Path(dst) + if "rollback-" in src_path.name and dst_path == install_dir: + raise OSError("restore failed") + return original_replace(src, dst) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT.os, "replace", flaky_replace) + + with pytest.raises( + PrebuiltFallback, + match = "activation and rollback failed; cleaned install state for fresh source build", + ): + activate_install_tree(staging_dir, install_dir, host) + + assert not install_dir.exists() + assert not staging_dir.exists() + assert not (tmp_path / ".staging").exists() + + output = capsys.readouterr().out + assert "rollback after failed activation also failed: restore failed" in output + assert ( + "cleaning staging, install, and rollback paths before source build fallback" + in output + ) + assert "removing failed install path" in output + assert "removing rollback path" in output + + +def test_binary_env_linux_includes_binary_parent_in_ld_library_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + bin_dir = install_dir / "build" / "bin" + bin_dir.mkdir(parents = True) + binary_path = bin_dir / "llama-server" + binary_path.write_bytes(b"fake") + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: []) + + env = binary_env(binary_path, install_dir, host) + ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep) + assert ( + str(bin_dir) in ld_dirs + ), f"binary_path.parent ({bin_dir}) must be in LD_LIBRARY_PATH, got: {ld_dirs}" + assert str(install_dir) in ld_dirs + + +def io_bytes(data: bytes): + return io.BytesIO(data) + + +def add_bytes_to_tar( + archive: tarfile.TarFile, name: str, data: bytes, *, mode: int = 0o644 +) -> None: + info = tarfile.TarInfo(name) + info.size = len(data) + info.mode = mode + archive.addfile(info, io_bytes(data)) + + +def add_symlink_to_tar(archive: tarfile.TarFile, name: str, target: str) -> None: + info = tarfile.TarInfo(name) + info.type = tarfile.SYMTYPE + info.linkname = target + archive.addfile(info) diff --git a/tests/studio/install/test_pr4562_bugfixes.py b/tests/studio/install/test_pr4562_bugfixes.py new file mode 100644 index 0000000000..9b8c6219de --- /dev/null +++ b/tests/studio/install/test_pr4562_bugfixes.py @@ -0,0 +1,687 @@ +""" +Comprehensive tests for PR #4562 bug fixes. + +Tests cover: + - Bug 1: PS1 detached HEAD on re-run (fetch + checkout -B pattern) + - Bug 2: Source-build fallback ignores pinned tag (both .sh and .ps1) + - Bug 3: Unix fallback deletes install before checking prerequisites + - Bug 4: Linux LD_LIBRARY_PATH missing build/bin + - "latest" tag resolution fallback chain (Unsloth -> ggml-org -> raw) + - Cross-platform binary_env (Linux, macOS, Windows) + - Edge cases: malformed JSON, empty responses, env overrides + +Run: pytest tests/studio/install/test_pr4562_bugfixes.py -v +""" + +import importlib.util +import json +import os +import subprocess +import sys +import textwrap +from pathlib import Path +from unittest.mock import patch + +import pytest + +# --------------------------------------------------------------------------- +# Load the module under test (same pattern as existing test files) +# --------------------------------------------------------------------------- +PACKAGE_ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py" +SPEC = importlib.util.spec_from_file_location( + "studio_install_llama_prebuilt", MODULE_PATH +) +assert SPEC is not None and SPEC.loader is not None +MOD = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = MOD +SPEC.loader.exec_module(MOD) + +binary_env = MOD.binary_env +HostInfo = MOD.HostInfo +resolve_requested_llama_tag = MOD.resolve_requested_llama_tag + +SETUP_SH = PACKAGE_ROOT / "studio" / "setup.sh" +SETUP_PS1 = PACKAGE_ROOT / "studio" / "setup.ps1" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def make_host(*, system: str) -> HostInfo: + """Create a HostInfo for the given OS.""" + return HostInfo( + system = system, + machine = "x86_64" if system != "Darwin" else "arm64", + is_windows = (system == "Windows"), + is_linux = (system == "Linux"), + is_macos = (system == "Darwin"), + is_x86_64 = (system != "Darwin"), + is_arm64 = (system == "Darwin"), + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + + +BASH = "/bin/bash" + + +def run_bash(script: str, *, timeout: int = 10, env: dict | None = None) -> str: + """Run a bash script fragment and return its stdout.""" + run_env = os.environ.copy() + if env: + run_env.update(env) + result = subprocess.run( + [BASH, "-c", script], + capture_output = True, + text = True, + timeout = timeout, + env = run_env, + ) + return result.stdout.strip() + + +# ========================================================================= +# TEST GROUP A: binary_env across all platforms (Bug 4 + cross-platform) +# ========================================================================= +class TestBinaryEnvCrossPlatform: + """Test that binary_env returns correct library paths for all OSes.""" + + def test_linux_includes_binary_parent_in_ld_library_path( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + install_dir = tmp_path / "llama.cpp" + bin_dir = install_dir / "build" / "bin" + bin_dir.mkdir(parents = True) + binary_path = bin_dir / "llama-server" + binary_path.write_bytes(b"fake") + + host = make_host(system = "Linux") + monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: []) + + env = binary_env(binary_path, install_dir, host) + ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep) + assert str(bin_dir) in ld_dirs, f"build/bin not in LD_LIBRARY_PATH: {ld_dirs}" + assert ( + str(install_dir) in ld_dirs + ), f"install_dir not in LD_LIBRARY_PATH: {ld_dirs}" + + def test_linux_binary_parent_comes_before_install_dir( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + """build/bin should be searched before install_dir for .so files.""" + install_dir = tmp_path / "llama.cpp" + bin_dir = install_dir / "build" / "bin" + bin_dir.mkdir(parents = True) + binary_path = bin_dir / "llama-server" + binary_path.write_bytes(b"fake") + + host = make_host(system = "Linux") + monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: []) + + env = binary_env(binary_path, install_dir, host) + ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep) + bin_idx = ld_dirs.index(str(bin_dir)) + install_idx = ld_dirs.index(str(install_dir)) + assert ( + bin_idx < install_idx + ), "binary_path.parent should come before install_dir" + + def test_linux_deduplicates_when_binary_parent_equals_install_dir( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + """When binary is directly in install_dir, no duplicate entries.""" + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir(parents = True) + binary_path = install_dir / "llama-server" + binary_path.write_bytes(b"fake") + + host = make_host(system = "Linux") + monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: []) + + env = binary_env(binary_path, install_dir, host) + ld_dirs = [d for d in env["LD_LIBRARY_PATH"].split(os.pathsep) if d] + count = ld_dirs.count(str(install_dir)) + assert count == 1, f"install_dir appears {count} times in LD_LIBRARY_PATH" + + def test_linux_preserves_existing_ld_library_path( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + install_dir = tmp_path / "llama.cpp" + bin_dir = install_dir / "build" / "bin" + bin_dir.mkdir(parents = True) + binary_path = bin_dir / "llama-server" + binary_path.write_bytes(b"fake") + + # Create real directories so dedupe_existing_dirs keeps them + custom_lib = tmp_path / "custom_lib" + other_lib = tmp_path / "other_lib" + custom_lib.mkdir() + other_lib.mkdir() + + host = make_host(system = "Linux") + monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: []) + original = os.environ.get("LD_LIBRARY_PATH", "") + os.environ["LD_LIBRARY_PATH"] = f"{custom_lib}:{other_lib}" + try: + env = binary_env(binary_path, install_dir, host) + finally: + if original: + os.environ["LD_LIBRARY_PATH"] = original + else: + os.environ.pop("LD_LIBRARY_PATH", None) + ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep) + assert str(custom_lib.resolve()) in ld_dirs + assert str(other_lib.resolve()) in ld_dirs + + def test_windows_includes_binary_parent_in_path( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + install_dir = tmp_path / "llama.cpp" + bin_dir = install_dir / "build" / "bin" / "Release" + bin_dir.mkdir(parents = True) + binary_path = bin_dir / "llama-server.exe" + binary_path.write_bytes(b"MZ") + + host = make_host(system = "Windows") + monkeypatch.setattr( + MOD, "windows_runtime_dirs_for_runtime_line", lambda _rt: [] + ) + + env = binary_env(binary_path, install_dir, host) + path_dirs = env["PATH"].split(os.pathsep) + assert str(bin_dir) in path_dirs, f"build/bin/Release not in PATH: {path_dirs}" + + def test_macos_sets_dyld_library_path( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir(parents = True) + bin_dir = install_dir / "build" / "bin" + binary_path = bin_dir / "llama-server" + binary_path.parent.mkdir(parents = True) + binary_path.write_bytes(b"fake") + + host = make_host(system = "Darwin") + monkeypatch.delenv("DYLD_LIBRARY_PATH", raising = False) + + env = binary_env(binary_path, install_dir, host) + dyld_parts = [p for p in env["DYLD_LIBRARY_PATH"].split(os.pathsep) if p] + assert ( + str(bin_dir) in dyld_parts + ), f"build/bin not in DYLD_LIBRARY_PATH: {dyld_parts}" + assert ( + str(install_dir) in dyld_parts + ), f"install_dir not in DYLD_LIBRARY_PATH: {dyld_parts}" + # binary_path.parent (build/bin) should come before install_dir + assert dyld_parts.index(str(bin_dir)) < dyld_parts.index(str(install_dir)) + + +# ========================================================================= +# TEST GROUP B: resolve_requested_llama_tag (Python function) +# ========================================================================= +class TestResolveRequestedLlamaTag: + def test_concrete_tag_passes_through(self): + assert resolve_requested_llama_tag("b8508") == "b8508" + + def test_none_resolves_to_latest(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b9999") + assert resolve_requested_llama_tag(None) == "b9999" + + def test_latest_resolves_to_upstream(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b1234") + assert resolve_requested_llama_tag("latest") == "b1234" + + def test_empty_string_resolves_to_latest(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b5555") + assert resolve_requested_llama_tag("") == "b5555" + + +# ========================================================================= +# TEST GROUP C: setup.sh logic (bash subprocess tests) +# ========================================================================= +class TestSetupShLogic: + """Test setup.sh fragments via bash subprocess with controlled PATH.""" + + def test_cmake_missing_preserves_install(self, tmp_path: Path): + """Bug 3: When cmake is missing, rm -rf should NOT run.""" + llama_dir = tmp_path / "llama.cpp" + llama_dir.mkdir() + marker = llama_dir / "marker.txt" + marker.write_text("existing") + + mock_bin = tmp_path / "mock_bin" + mock_bin.mkdir() + # Create mock git but NOT cmake + (mock_bin / "git").write_text("#!/bin/bash\nexit 0\n") + (mock_bin / "git").chmod(0o755) + + # Build PATH: mock_bin first, then system dirs WITHOUT cmake + safe_dirs = [str(mock_bin)] + for d in os.environ.get("PATH", "").split(":"): + if d and not os.path.isfile(os.path.join(d, "cmake")): + safe_dirs.append(d) + + script = textwrap.dedent(f"""\ + export LLAMA_CPP_DIR="{llama_dir}" + if ! command -v cmake &>/dev/null; then + echo "cmake_missing" + elif ! command -v git &>/dev/null; then + echo "git_missing" + else + rm -rf "$LLAMA_CPP_DIR" + echo "would_clone" + fi + """) + output = run_bash(script, env = {"PATH": ":".join(safe_dirs)}) + assert "cmake_missing" in output + assert marker.exists(), "Install dir was deleted despite cmake missing!" + + def test_git_missing_preserves_install(self, tmp_path: Path): + """Bug 3: When git is missing, rm -rf should NOT run.""" + llama_dir = tmp_path / "llama.cpp" + llama_dir.mkdir() + marker = llama_dir / "marker.txt" + marker.write_text("existing") + + mock_bin = tmp_path / "mock_bin" + mock_bin.mkdir() + # Create mock cmake but NOT git + (mock_bin / "cmake").write_text("#!/bin/bash\nexit 0\n") + (mock_bin / "cmake").chmod(0o755) + + # Build PATH: mock_bin first, then system dirs WITHOUT git + safe_dirs = [str(mock_bin)] + for d in os.environ.get("PATH", "").split(":"): + if d and not os.path.isfile(os.path.join(d, "git")): + safe_dirs.append(d) + + script = textwrap.dedent(f"""\ + export LLAMA_CPP_DIR="{llama_dir}" + if ! command -v cmake &>/dev/null; then + echo "cmake_missing" + elif ! command -v git &>/dev/null; then + echo "git_missing" + else + rm -rf "$LLAMA_CPP_DIR" + echo "would_clone" + fi + """) + output = run_bash(script, env = {"PATH": ":".join(safe_dirs)}) + assert "git_missing" in output + assert marker.exists(), "Install dir was deleted despite git missing!" + + def test_both_present_runs_rm_and_clone(self, tmp_path: Path): + """Bug 3: When both present, rm -rf runs before clone.""" + llama_dir = tmp_path / "llama.cpp" + llama_dir.mkdir() + marker = llama_dir / "marker.txt" + marker.write_text("existing") + + mock_bin = tmp_path / "mock_bin" + mock_bin.mkdir() + (mock_bin / "cmake").write_text("#!/bin/bash\nexit 0\n") + (mock_bin / "cmake").chmod(0o755) + (mock_bin / "git").write_text("#!/bin/bash\nexit 0\n") + (mock_bin / "git").chmod(0o755) + + script = textwrap.dedent(f"""\ + export PATH="{mock_bin}:$PATH" + export LLAMA_CPP_DIR="{llama_dir}" + if ! command -v cmake &>/dev/null; then + echo "cmake_missing" + elif ! command -v git &>/dev/null; then + echo "git_missing" + else + rm -rf "$LLAMA_CPP_DIR" + echo "would_clone" + fi + """) + output = run_bash(script) + assert "would_clone" in output + assert not marker.exists(), "Install dir should have been deleted" + + def test_clone_uses_pinned_tag(self, tmp_path: Path): + """Bug 2: git clone should use --branch with the resolved tag.""" + mock_bin = tmp_path / "mock_bin" + mock_bin.mkdir() + log_file = tmp_path / "git_calls.log" + (mock_bin / "git").write_text(f'#!/bin/bash\necho "$*" >> {log_file}\nexit 0\n') + (mock_bin / "git").chmod(0o755) + + script = textwrap.dedent(f"""\ + export PATH="{mock_bin}:$PATH" + git clone --depth 1 --branch "b8508" https://github.com/ggml-org/llama.cpp.git /tmp/llama_test + """) + run_bash(script) + log = log_file.read_text() + assert "--branch b8508" in log, f"Expected --branch b8508 in: {log}" + + def test_fetch_checkout_b_pattern(self, tmp_path: Path): + """Bug 1: Re-run should use fetch + checkout -B, not pull + checkout FETCH_HEAD.""" + mock_bin = tmp_path / "mock_bin" + mock_bin.mkdir() + log_file = tmp_path / "git_calls.log" + (mock_bin / "git").write_text(f'#!/bin/bash\necho "$*" >> {log_file}\nexit 0\n') + (mock_bin / "git").chmod(0o755) + + llama_dir = tmp_path / "llama.cpp" + llama_dir.mkdir() + (llama_dir / ".git").mkdir() + + script = textwrap.dedent(f"""\ + export PATH="{mock_bin}:$PATH" + LlamaCppDir="{llama_dir}" + ResolvedLlamaTag="b8508" + if [ -d "$LlamaCppDir/.git" ]; then + git -C "$LlamaCppDir" fetch --depth 1 origin "$ResolvedLlamaTag" + if [ $? -ne 0 ]; then + echo "WARN: fetch failed" + else + git -C "$LlamaCppDir" checkout -B unsloth-llama-build FETCH_HEAD + fi + fi + """) + run_bash(script) + log = log_file.read_text() + assert "fetch --depth 1 origin b8508" in log + assert "checkout -B unsloth-llama-build FETCH_HEAD" in log + assert "pull" not in log, "Should use fetch, not pull" + + def test_fetch_failure_warns_not_aborts(self, tmp_path: Path): + """Bug 1: fetch failure should warn and continue, not set BuildOk=false.""" + mock_bin = tmp_path / "mock_bin" + mock_bin.mkdir() + (mock_bin / "git").write_text( + '#!/bin/bash\nif echo "$*" | grep -q fetch; then exit 1; fi\nexit 0\n' + ) + (mock_bin / "git").chmod(0o755) + + llama_dir = tmp_path / "llama.cpp" + llama_dir.mkdir() + (llama_dir / ".git").mkdir() + + script = textwrap.dedent(f"""\ + export PATH="{mock_bin}:$PATH" + LlamaCppDir="{llama_dir}" + ResolvedLlamaTag="b8508" + BuildOk=true + if [ -d "$LlamaCppDir/.git" ]; then + git -C "$LlamaCppDir" fetch --depth 1 origin "$ResolvedLlamaTag" + if [ $? -ne 0 ]; then + echo "WARN: fetch failed -- using existing source" + else + git -C "$LlamaCppDir" checkout -B unsloth-llama-build FETCH_HEAD + fi + fi + echo "BuildOk=$BuildOk" + """) + output = run_bash(script) + assert "WARN: fetch failed" in output + assert "BuildOk=true" in output + + +# ========================================================================= +# TEST GROUP D: "latest" tag resolution (bash subprocess) +# ========================================================================= +class TestLatestTagResolution: + """Test the fallback chain: Unsloth API -> ggml-org API -> raw.""" + + RESOLVE_TEMPLATE = textwrap.dedent("""\ + export PATH="{mock_bin}:$PATH" + _REQUESTED_LLAMA_TAG="{requested_tag}" + _RESOLVED_LLAMA_TAG="" + _RESOLVE_UPSTREAM_STATUS=1 + _HELPER_RELEASE_REPO="unslothai/llama.cpp" + if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then + if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then + _RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${{_HELPER_RELEASE_REPO}}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" + if [ -z "$_RESOLVED_LLAMA_TAG" ]; then + _RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" + fi + fi + if [ -z "$_RESOLVED_LLAMA_TAG" ]; then + _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" + fi + fi + echo "$_RESOLVED_LLAMA_TAG" + """) + + @staticmethod + def _make_curl_mock( + mock_bin: Path, unsloth_response: str | None, ggml_response: str | None + ): + """Create a curl mock that returns different responses per repo.""" + lines = ["#!/bin/bash"] + if unsloth_response is not None: + lines.append( + f'if echo "$*" | grep -q "unslothai/llama.cpp"; then echo \'{unsloth_response}\'; exit 0; fi' + ) + else: + lines.append( + 'if echo "$*" | grep -q "unslothai/llama.cpp"; then exit 1; fi' + ) + if ggml_response is not None: + lines.append( + f'if echo "$*" | grep -q "ggml-org/llama.cpp"; then echo \'{ggml_response}\'; exit 0; fi' + ) + else: + lines.append('if echo "$*" | grep -q "ggml-org/llama.cpp"; then exit 1; fi') + lines.append("exit 1") + curl_path = mock_bin / "curl" + curl_path.write_text("\n".join(lines) + "\n") + curl_path.chmod(0o755) + + def _run_resolve( + self, + tmp_path: Path, + requested_tag: str, + unsloth_resp: str | None, + ggml_resp: str | None, + ) -> str: + mock_bin = tmp_path / "mock_bin" + mock_bin.mkdir(exist_ok = True) + self._make_curl_mock(mock_bin, unsloth_resp, ggml_resp) + script = self.RESOLVE_TEMPLATE.format( + mock_bin = mock_bin, requested_tag = requested_tag + ) + return run_bash(script) + + def test_unsloth_succeeds(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "latest", + unsloth_resp = '{"tag_name":"b8508"}', + ggml_resp = '{"tag_name":"b9000"}', + ) + assert output == "b8508" + + def test_unsloth_fails_ggml_succeeds(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "latest", + unsloth_resp = None, + ggml_resp = '{"tag_name":"b9000"}', + ) + assert output == "b9000" + + def test_both_fail_raw_fallback(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "latest", + unsloth_resp = None, + ggml_resp = None, + ) + assert output == "latest" + + def test_concrete_tag_passes_through(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "b7777", + unsloth_resp = '{"tag_name":"b8508"}', + ggml_resp = '{"tag_name":"b9000"}', + ) + assert output == "b7777" + + def test_unsloth_malformed_json_falls_through(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "latest", + unsloth_resp = '{"bad_key":"no_tag"}', + ggml_resp = '{"tag_name":"b9001"}', + ) + assert output == "b9001" + + def test_both_malformed_json_raw_fallback(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "latest", + unsloth_resp = '{"bad":"data"}', + ggml_resp = '{"also":"bad"}', + ) + assert output == "latest" + + def test_unsloth_empty_body_falls_through(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "latest", + unsloth_resp = "", + ggml_resp = '{"tag_name":"b7000"}', + ) + assert output == "b7000" + + def test_unsloth_empty_tag_name_falls_through(self, tmp_path: Path): + output = self._run_resolve( + tmp_path, + "latest", + unsloth_resp = '{"tag_name":""}', + ggml_resp = '{"tag_name":"b6000"}', + ) + assert output == "b6000" + + def test_env_override_unsloth_llama_tag(self): + output = run_bash( + 'echo "${UNSLOTH_LLAMA_TAG:-latest}"', + env = {"UNSLOTH_LLAMA_TAG": "b1234"}, + ) + assert output == "b1234" + + def test_env_unset_defaults_to_latest(self): + env = os.environ.copy() + env.pop("UNSLOTH_LLAMA_TAG", None) + output = run_bash('echo "${UNSLOTH_LLAMA_TAG:-latest}"', env = env) + assert output == "latest" + + def test_env_empty_defaults_to_latest(self): + output = run_bash( + 'echo "${UNSLOTH_LLAMA_TAG:-latest}"', + env = {"UNSLOTH_LLAMA_TAG": ""}, + ) + assert output == "latest" + + +# ========================================================================= +# TEST GROUP E: Source file verification +# ========================================================================= +class TestSourceCodePatterns: + """Verify the actual source files contain the expected fix patterns.""" + + def test_setup_sh_no_rm_before_prereq_check(self): + """rm -rf must appear AFTER cmake/git checks, not before.""" + content = SETUP_SH.read_text() + # Find the source-build block + idx_else = content.find("# Check prerequisites") + assert idx_else != -1 + block = content[idx_else:] + # rm -rf should appear after the cmake/git checks + idx_cmake = block.find("command -v cmake") + idx_git = block.find("command -v git") + idx_rm = block.find("rm -rf") + assert idx_rm > idx_cmake, "rm -rf should come after cmake check" + assert idx_rm > idx_git, "rm -rf should come after git check" + + def test_setup_sh_clone_uses_branch_tag(self): + """git clone in source-build should use --branch via _CLONE_BRANCH_ARGS.""" + content = SETUP_SH.read_text() + # The clone line should use _CLONE_BRANCH_ARGS (which conditionally includes --branch) + assert ( + "_CLONE_BRANCH_ARGS" in content + ), "Clone should use _CLONE_BRANCH_ARGS array" + assert ( + '--branch "$_RESOLVED_LLAMA_TAG"' in content + ), "_CLONE_BRANCH_ARGS should be set to --branch $_RESOLVED_LLAMA_TAG" + # Verify the guard: --branch is only used when tag is not "latest" + assert ( + '_RESOLVED_LLAMA_TAG" != "latest"' in content + ), "Should guard against literal 'latest' tag" + + def test_setup_sh_latest_resolution_queries_unsloth_first(self): + """The Unsloth repo should be queried before ggml-org.""" + content = SETUP_SH.read_text() + idx_unsloth = content.find("_HELPER_RELEASE_REPO}/releases/latest") + idx_ggml = content.find("ggml-org/llama.cpp/releases/latest") + assert idx_unsloth != -1, "Unsloth API query not found" + assert idx_ggml != -1, "ggml-org API query not found" + assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org" + + def test_setup_ps1_uses_checkout_b(self): + """PS1 should use checkout -B, not checkout --force FETCH_HEAD.""" + content = SETUP_PS1.read_text() + assert "checkout -B unsloth-llama-build" in content + assert "checkout --force FETCH_HEAD" not in content + + def test_setup_ps1_clone_uses_branch_tag(self): + """PS1 clone should use --branch with the resolved tag.""" + content = SETUP_PS1.read_text() + assert "--branch" in content and "$ResolvedLlamaTag" in content + # The old commented-out line should be gone + assert "# git clone --depth 1 --branch" not in content + + def test_setup_ps1_no_git_pull(self): + """PS1 should use fetch, not pull (which fails in detached HEAD).""" + content = SETUP_PS1.read_text() + # In the source-build section, there should be no "git pull" + # (git pull is only valid on a branch) + lines = content.splitlines() + for i, line in enumerate(lines): + stripped = line.strip() + if "git pull" in stripped and not stripped.startswith("#"): + # Check context -- should not be in the llama.cpp build section + # Allow git pull in other contexts + context = "\n".join(lines[max(0, i - 5) : i + 5]) + if "LlamaCppDir" in context: + pytest.fail( + f"Found 'git pull' in llama.cpp build section at line {i+1}" + ) + + def test_setup_ps1_latest_resolution_queries_unsloth_first(self): + """PS1 should query Unsloth repo before ggml-org.""" + content = SETUP_PS1.read_text() + idx_unsloth = content.find("$HelperReleaseRepo/releases/latest") + idx_ggml = content.find("ggml-org/llama.cpp/releases/latest") + assert idx_unsloth != -1, "Unsloth API query not found in PS1" + assert idx_ggml != -1, "ggml-org API query not found in PS1" + assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org" + + def test_binary_env_linux_has_binary_parent(self): + """The Linux branch of binary_env should include binary_path.parent.""" + content = MODULE_PATH.read_text() + # Find the binary_env function + in_func = False + in_linux = False + found = False + for line in content.splitlines(): + if "def binary_env(" in line: + in_func = True + elif in_func and line and not line[0].isspace() and "def " in line: + break + if in_func and "host.is_linux" in line: + in_linux = True + if in_linux and "binary_path.parent" in line: + found = True + break + assert found, "binary_path.parent not found in Linux branch of binary_env" diff --git a/tests/studio/install/test_selection_logic.py b/tests/studio/install/test_selection_logic.py new file mode 100644 index 0000000000..906c978b0d --- /dev/null +++ b/tests/studio/install/test_selection_logic.py @@ -0,0 +1,903 @@ +"""Tests for binary selection logic in install_llama_prebuilt.py. + +Covers: normalize_compute_cap, normalize_compute_caps, parse_cuda_visible_devices, +supports_explicit_visible_device_matching, select_visible_gpu_rows, +compatible_linux_runtime_lines, pick_windows_cuda_runtime, +compatible_windows_runtime_lines, runtime_line_from_cuda_version, +apply_approved_hashes, linux_cuda_choice_from_release, windows_cuda_attempts, +resolve_upstream_asset_choice. + +No GPU, no network, no torch required -- all I/O is monkeypatched. +""" + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +PACKAGE_ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py" +SPEC = importlib.util.spec_from_file_location( + "studio_install_llama_prebuilt", MODULE_PATH +) +assert SPEC is not None and SPEC.loader is not None +INSTALL_LLAMA_PREBUILT = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = INSTALL_LLAMA_PREBUILT +SPEC.loader.exec_module(INSTALL_LLAMA_PREBUILT) + +HostInfo = INSTALL_LLAMA_PREBUILT.HostInfo +AssetChoice = INSTALL_LLAMA_PREBUILT.AssetChoice +PublishedLlamaArtifact = INSTALL_LLAMA_PREBUILT.PublishedLlamaArtifact +PublishedReleaseBundle = INSTALL_LLAMA_PREBUILT.PublishedReleaseBundle +ApprovedArtifactHash = INSTALL_LLAMA_PREBUILT.ApprovedArtifactHash +ApprovedReleaseChecksums = INSTALL_LLAMA_PREBUILT.ApprovedReleaseChecksums +PrebuiltFallback = INSTALL_LLAMA_PREBUILT.PrebuiltFallback +LinuxCudaSelection = INSTALL_LLAMA_PREBUILT.LinuxCudaSelection +UPSTREAM_REPO = INSTALL_LLAMA_PREBUILT.UPSTREAM_REPO + +normalize_compute_cap = INSTALL_LLAMA_PREBUILT.normalize_compute_cap +normalize_compute_caps = INSTALL_LLAMA_PREBUILT.normalize_compute_caps +parse_cuda_visible_devices = INSTALL_LLAMA_PREBUILT.parse_cuda_visible_devices +supports_explicit_visible_device_matching = ( + INSTALL_LLAMA_PREBUILT.supports_explicit_visible_device_matching +) +select_visible_gpu_rows = INSTALL_LLAMA_PREBUILT.select_visible_gpu_rows +compatible_linux_runtime_lines = INSTALL_LLAMA_PREBUILT.compatible_linux_runtime_lines +pick_windows_cuda_runtime = INSTALL_LLAMA_PREBUILT.pick_windows_cuda_runtime +compatible_windows_runtime_lines = ( + INSTALL_LLAMA_PREBUILT.compatible_windows_runtime_lines +) +runtime_line_from_cuda_version = INSTALL_LLAMA_PREBUILT.runtime_line_from_cuda_version +apply_approved_hashes = INSTALL_LLAMA_PREBUILT.apply_approved_hashes +linux_cuda_choice_from_release = INSTALL_LLAMA_PREBUILT.linux_cuda_choice_from_release +windows_cuda_attempts = INSTALL_LLAMA_PREBUILT.windows_cuda_attempts +resolve_upstream_asset_choice = INSTALL_LLAMA_PREBUILT.resolve_upstream_asset_choice + + +# --------------------------------------------------------------------------- +# Helper factories +# --------------------------------------------------------------------------- + + +def make_host(**overrides): + system = overrides.pop("system", "Linux") + machine = overrides.pop("machine", "x86_64") + defaults = dict( + system = system, + machine = machine, + is_linux = system == "Linux", + is_windows = system == "Windows", + is_macos = system == "Darwin", + is_x86_64 = machine.lower() in {"x86_64", "amd64"}, + is_arm64 = machine.lower() in {"arm64", "aarch64"}, + nvidia_smi = "/usr/bin/nvidia-smi", + driver_cuda_version = (12, 8), + compute_caps = ["86"], + visible_cuda_devices = None, + has_physical_nvidia = True, + has_usable_nvidia = True, + ) + defaults.update(overrides) + return HostInfo(**defaults) + + +def make_artifact(asset_name, **overrides): + defaults = dict( + asset_name = asset_name, + install_kind = "linux-cuda", + runtime_line = "cuda12", + coverage_class = "targeted", + supported_sms = ["75", "80", "86", "89", "90"], + min_sm = 75, + max_sm = 90, + bundle_profile = "cuda12-newer", + rank = 100, + ) + defaults.update(overrides) + return PublishedLlamaArtifact(**defaults) + + +def make_release(artifacts, **overrides): + defaults = dict( + repo = "unslothai/llama.cpp", + release_tag = "v1.0", + upstream_tag = "b8508", + assets = {a.asset_name: f"https://example.com/{a.asset_name}" for a in artifacts}, + manifest_asset_name = "llama-prebuilt-manifest.json", + artifacts = artifacts, + selection_log = [], + ) + defaults.update(overrides) + return PublishedReleaseBundle(**defaults) + + +def make_checksums(asset_names): + return ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "v1.0", + upstream_tag = "b8508", + source_commit = None, + artifacts = { + name: ApprovedArtifactHash( + asset_name = name, + sha256 = "a" * 64, + repo = "unslothai/llama.cpp", + kind = "prebuilt", + ) + for name in asset_names + }, + ) + + +def mock_linux_runtime(monkeypatch, lines): + dirs = {line: ["/usr/lib/stub"] for line in lines} + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "detected_linux_runtime_lines", + lambda: (list(lines), dict(dirs)), + ) + + +def mock_windows_runtime(monkeypatch, lines): + dirs = {line: ["C:\\Windows\\System32"] for line in lines} + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "detected_windows_runtime_lines", + lambda: (list(lines), dict(dirs)), + ) + + +# =========================================================================== +# A. normalize_compute_cap +# =========================================================================== + + +class TestNormalizeComputeCap: + def test_dotted_86(self): + assert normalize_compute_cap("8.6") == "86" + + def test_dotted_leading_zero(self): + assert normalize_compute_cap("07.05") == "75" + + def test_already_normalized(self): + assert normalize_compute_cap("75") == "75" + + def test_int_input(self): + assert normalize_compute_cap(86) == "86" + + def test_empty_string(self): + assert normalize_compute_cap("") is None + + def test_whitespace(self): + assert normalize_compute_cap(" ") is None + + def test_non_numeric(self): + assert normalize_compute_cap("x.y") is None + + def test_triple_part(self): + assert normalize_compute_cap("8.6.0") is None + + def test_zero_minor(self): + assert normalize_compute_cap("9.0") == "90" + + +# =========================================================================== +# B. normalize_compute_caps +# =========================================================================== + + +class TestNormalizeComputeCaps: + def test_deduplication(self): + assert normalize_compute_caps(["8.6", "86", "8.6"]) == ["86"] + + def test_numeric_sort(self): + assert normalize_compute_caps(["9.0", "7.5", "8.6"]) == ["75", "86", "90"] + + def test_drops_invalid(self): + assert normalize_compute_caps(["8.6", "bad", "", "7.5"]) == ["75", "86"] + + def test_empty_input(self): + assert normalize_compute_caps([]) == [] + + +# =========================================================================== +# C. parse_cuda_visible_devices +# =========================================================================== + + +class TestParseCudaVisibleDevices: + def test_none(self): + assert parse_cuda_visible_devices(None) is None + + def test_empty(self): + assert parse_cuda_visible_devices("") == [] + + def test_minus_one(self): + assert parse_cuda_visible_devices("-1") == [] + + def test_single(self): + assert parse_cuda_visible_devices("0") == ["0"] + + def test_multi(self): + assert parse_cuda_visible_devices("0,1,2") == ["0", "1", "2"] + + def test_whitespace_stripped(self): + assert parse_cuda_visible_devices(" 0 , 1 ") == ["0", "1"] + + +# =========================================================================== +# D. supports_explicit_visible_device_matching +# =========================================================================== + + +class TestSupportsExplicitVisibleDeviceMatching: + def test_all_digits(self): + assert supports_explicit_visible_device_matching(["0", "1", "2"]) is True + + def test_gpu_prefix(self): + assert supports_explicit_visible_device_matching(["GPU-abc123"]) is True + + def test_none(self): + assert supports_explicit_visible_device_matching(None) is False + + def test_empty(self): + assert supports_explicit_visible_device_matching([]) is False + + def test_mixed_invalid(self): + assert supports_explicit_visible_device_matching(["0", "MIG-device"]) is False + + +# =========================================================================== +# E. select_visible_gpu_rows +# =========================================================================== + + +class TestSelectVisibleGpuRows: + ROWS = [ + ("0", "GPU-aaa", "8.6"), + ("1", "GPU-bbb", "7.5"), + ("2", "GPU-ccc", "8.9"), + ] + + def test_none_returns_all(self): + assert select_visible_gpu_rows(self.ROWS, None) == list(self.ROWS) + + def test_empty_returns_empty(self): + assert select_visible_gpu_rows(self.ROWS, []) == [] + + def test_filter_by_index(self): + result = select_visible_gpu_rows(self.ROWS, ["0", "2"]) + assert result == [("0", "GPU-aaa", "8.6"), ("2", "GPU-ccc", "8.9")] + + def test_filter_by_uuid_case_insensitive(self): + result = select_visible_gpu_rows(self.ROWS, ["gpu-bbb"]) + assert result == [("1", "GPU-bbb", "7.5")] + + def test_dedup_same_device(self): + result = select_visible_gpu_rows(self.ROWS, ["0", "0"]) + assert result == [("0", "GPU-aaa", "8.6")] + + def test_missing_token(self): + result = select_visible_gpu_rows(self.ROWS, ["99"]) + assert result == [] + + +# =========================================================================== +# F. compatible_linux_runtime_lines +# =========================================================================== + + +class TestCompatibleLinuxRuntimeLines: + def test_no_driver(self): + host = make_host(driver_cuda_version = None) + assert compatible_linux_runtime_lines(host) == [] + + def test_driver_11_8(self): + host = make_host(driver_cuda_version = (11, 8)) + assert compatible_linux_runtime_lines(host) == [] + + def test_driver_12_4(self): + host = make_host(driver_cuda_version = (12, 4)) + assert compatible_linux_runtime_lines(host) == ["cuda12"] + + def test_driver_13_0(self): + host = make_host(driver_cuda_version = (13, 0)) + assert compatible_linux_runtime_lines(host) == ["cuda13", "cuda12"] + + +# =========================================================================== +# G. pick_windows_cuda_runtime + compatible_windows_runtime_lines +# =========================================================================== + + +class TestPickWindowsCudaRuntime: + def test_no_driver(self): + host = make_host(driver_cuda_version = None) + assert pick_windows_cuda_runtime(host) is None + + def test_below_threshold(self): + host = make_host(driver_cuda_version = (12, 3)) + assert pick_windows_cuda_runtime(host) is None + + def test_driver_12_4(self): + host = make_host(driver_cuda_version = (12, 4)) + assert pick_windows_cuda_runtime(host) == "12.4" + + def test_driver_13_1(self): + host = make_host(driver_cuda_version = (13, 1)) + assert pick_windows_cuda_runtime(host) == "13.1" + + +class TestCompatibleWindowsRuntimeLines: + def test_no_driver(self): + host = make_host(driver_cuda_version = None) + assert compatible_windows_runtime_lines(host) == [] + + def test_driver_12_4(self): + host = make_host(driver_cuda_version = (12, 4)) + assert compatible_windows_runtime_lines(host) == ["cuda12"] + + def test_driver_13_1(self): + host = make_host(driver_cuda_version = (13, 1)) + assert compatible_windows_runtime_lines(host) == ["cuda13", "cuda12"] + + +# =========================================================================== +# H. runtime_line_from_cuda_version +# =========================================================================== + + +class TestRuntimeLineFromCudaVersion: + def test_cuda_12(self): + assert runtime_line_from_cuda_version("12.6") == "cuda12" + + def test_cuda_13(self): + assert runtime_line_from_cuda_version("13.0") == "cuda13" + + def test_cuda_11(self): + assert runtime_line_from_cuda_version("11.8") is None + + def test_none(self): + assert runtime_line_from_cuda_version(None) is None + + def test_empty(self): + assert runtime_line_from_cuda_version("") is None + + +# =========================================================================== +# I. apply_approved_hashes +# =========================================================================== + + +class TestApplyApprovedHashes: + def _choice(self, name): + return AssetChoice( + repo = "test", + tag = "v1", + name = name, + url = f"https://x/{name}", + source_label = "test", + ) + + def test_both_approved(self): + c1, c2 = self._choice("a.tar.gz"), self._choice("b.tar.gz") + checksums = make_checksums(["a.tar.gz", "b.tar.gz"]) + result = apply_approved_hashes([c1, c2], checksums) + assert len(result) == 2 + assert all(c.expected_sha256 == "a" * 64 for c in result) + + def test_one_approved(self): + c1, c2 = self._choice("a.tar.gz"), self._choice("missing.tar.gz") + checksums = make_checksums(["a.tar.gz"]) + result = apply_approved_hashes([c1, c2], checksums) + assert len(result) == 1 + assert result[0].name == "a.tar.gz" + + def test_none_approved(self): + c1 = self._choice("missing.tar.gz") + checksums = make_checksums(["other.tar.gz"]) + with pytest.raises(PrebuiltFallback, match = "approved checksum"): + apply_approved_hashes([c1], checksums) + + def test_empty_input(self): + checksums = make_checksums(["a.tar.gz"]) + with pytest.raises(PrebuiltFallback, match = "approved checksum"): + apply_approved_hashes([], checksums) + + +# =========================================================================== +# J. linux_cuda_choice_from_release -- core selection +# =========================================================================== + + +class TestLinuxCudaChoiceFromRelease: + # --- Runtime line resolution --- + + def test_no_runtime_lines_detected(self, monkeypatch): + mock_linux_runtime(monkeypatch, []) + host = make_host(driver_cuda_version = (12, 8)) + art = make_artifact("bundle-cuda12.tar.gz") + release = make_release([art]) + assert linux_cuda_choice_from_release(host, release) is None + + def test_detected_lines_incompatible_with_driver(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda13"]) + host = make_host(driver_cuda_version = (12, 4)) + art = make_artifact("bundle-cuda13.tar.gz", runtime_line = "cuda13") + release = make_release([art]) + assert linux_cuda_choice_from_release(host, release) is None + + def test_driver_13_only_cuda12_detected(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(driver_cuda_version = (13, 0)) + art = make_artifact("bundle-cuda12.tar.gz", runtime_line = "cuda12") + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is not None + assert result.primary.runtime_line == "cuda12" + + def test_preferred_runtime_line_reorders(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda13", "cuda12"]) + host = make_host(driver_cuda_version = (13, 0)) + art12 = make_artifact("bundle-cuda12.tar.gz", runtime_line = "cuda12") + art13 = make_artifact("bundle-cuda13.tar.gz", runtime_line = "cuda13") + release = make_release([art12, art13]) + result = linux_cuda_choice_from_release( + host, release, preferred_runtime_line = "cuda12" + ) + assert result is not None + assert result.primary.runtime_line == "cuda12" + + def test_preferred_runtime_line_unavailable(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(driver_cuda_version = (12, 8)) + art = make_artifact("bundle-cuda12.tar.gz", runtime_line = "cuda12") + release = make_release([art]) + result = linux_cuda_choice_from_release( + host, release, preferred_runtime_line = "cuda13" + ) + assert result is not None + assert result.primary.runtime_line == "cuda12" + log_entries = result.selection_log + assert any("unavailable_on_host" in entry for entry in log_entries) + + # --- SM matching --- + + def test_exact_sm_match(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + art = make_artifact( + "bundle.tar.gz", supported_sms = ["75", "86", "89"], min_sm = 75, max_sm = 89 + ) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is not None + assert result.primary.name == "bundle.tar.gz" + + def test_sm_not_in_supported_sms(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + art = make_artifact( + "bundle.tar.gz", supported_sms = ["75", "80", "89"], min_sm = 75, max_sm = 89 + ) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_sm_outside_min_range(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["50"]) + art = make_artifact( + "bundle.tar.gz", supported_sms = ["50", "75", "86"], min_sm = 75, max_sm = 90 + ) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_sm_outside_max_range(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["100"]) + art = make_artifact( + "bundle.tar.gz", supported_sms = ["100", "75", "86"], min_sm = 75, max_sm = 90 + ) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_very_old_sm(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["50"]) + art = make_artifact("bundle.tar.gz", min_sm = 75, max_sm = 90) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_very_new_sm(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["100"]) + art = make_artifact("bundle.tar.gz", min_sm = 75, max_sm = 90) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + # --- Unknown compute caps (empty list) --- + + def test_unknown_caps_only_portable(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = []) + targeted = make_artifact("targeted.tar.gz", coverage_class = "targeted") + portable = make_artifact("portable.tar.gz", coverage_class = "portable") + release = make_release([targeted, portable]) + result = linux_cuda_choice_from_release(host, release) + assert result is not None + assert result.primary.name == "portable.tar.gz" + + def test_unknown_caps_no_portable(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = []) + targeted = make_artifact("targeted.tar.gz", coverage_class = "targeted") + release = make_release([targeted]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + # --- Multi-GPU --- + + def test_multi_gpu_all_covered(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["75", "89"]) + art = make_artifact( + "bundle.tar.gz", + supported_sms = ["75", "80", "86", "89", "90"], + min_sm = 75, + max_sm = 90, + ) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is not None + + def test_multi_gpu_not_all_covered(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["50", "89"]) + art = make_artifact( + "bundle.tar.gz", supported_sms = ["75", "89"], min_sm = 75, max_sm = 89 + ) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + # --- Artifact selection priority --- + + def test_narrowest_sm_range_wins(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + wide = make_artifact( + "wide.tar.gz", + supported_sms = ["75", "86", "90"], + min_sm = 75, + max_sm = 90, + rank = 100, + ) + narrow = make_artifact( + "narrow.tar.gz", + supported_sms = ["80", "86", "89"], + min_sm = 80, + max_sm = 89, + rank = 100, + ) + release = make_release([wide, narrow]) + result = linux_cuda_choice_from_release(host, release) + assert result is not None + assert result.primary.name == "narrow.tar.gz" + + def test_range_tie_lower_rank_wins(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + high = make_artifact( + "high.tar.gz", + supported_sms = ["75", "86", "90"], + min_sm = 75, + max_sm = 90, + rank = 200, + ) + low = make_artifact( + "low.tar.gz", + supported_sms = ["75", "86", "90"], + min_sm = 75, + max_sm = 90, + rank = 50, + ) + release = make_release([high, low]) + result = linux_cuda_choice_from_release(host, release) + assert result is not None + assert result.primary.name == "low.tar.gz" + + def test_targeted_preferred_portable_fallback(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + targeted = make_artifact("targeted.tar.gz", coverage_class = "targeted", rank = 100) + portable = make_artifact("portable.tar.gz", coverage_class = "portable", rank = 100) + release = make_release([targeted, portable]) + result = linux_cuda_choice_from_release(host, release) + assert result is not None + assert result.primary.name == "targeted.tar.gz" + assert len(result.attempts) == 2 + assert result.attempts[1].name == "portable.tar.gz" + + # --- Edge cases --- + + def test_asset_missing_from_release_assets(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + art = make_artifact("bundle.tar.gz") + release = make_release([art], assets = {}) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_artifact_empty_supported_sms(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + art = make_artifact("bundle.tar.gz", supported_sms = []) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_artifact_missing_min_sm(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + art = make_artifact("bundle.tar.gz", min_sm = None, max_sm = 90) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_artifact_missing_max_sm(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + art = make_artifact("bundle.tar.gz", min_sm = 75, max_sm = None) + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_no_linux_cuda_artifacts(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + art = make_artifact("bundle.tar.gz", install_kind = "windows-cuda") + release = make_release([art]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + def test_empty_artifacts_list(self, monkeypatch): + mock_linux_runtime(monkeypatch, ["cuda12"]) + host = make_host(compute_caps = ["86"]) + release = make_release([]) + result = linux_cuda_choice_from_release(host, release) + assert result is None + + +# =========================================================================== +# K. windows_cuda_attempts +# =========================================================================== + + +class TestWindowsCudaAttempts: + TAG = "b8508" + + def _upstream(self, *runtime_versions): + assets = {} + for rv in runtime_versions: + name = f"llama-{self.TAG}-bin-win-cuda-{rv}-x64.zip" + assets[name] = f"https://example.com/{name}" + return assets + + def test_driver_12_4_no_dlls_fallback(self, monkeypatch): + mock_windows_runtime(monkeypatch, []) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4)) + assets = self._upstream("12.4") + result = windows_cuda_attempts(host, self.TAG, assets, None) + assert len(result) == 1 + assert result[0].runtime_line == "cuda12" + + def test_driver_13_1_both_dlls(self, monkeypatch): + mock_windows_runtime(monkeypatch, ["cuda13", "cuda12"]) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (13, 1)) + assets = self._upstream("13.1", "12.4") + result = windows_cuda_attempts(host, self.TAG, assets, None) + assert len(result) == 2 + assert result[0].runtime_line == "cuda13" + assert result[1].runtime_line == "cuda12" + + def test_preferred_reorders(self, monkeypatch): + mock_windows_runtime(monkeypatch, ["cuda13", "cuda12"]) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (13, 1)) + assets = self._upstream("13.1", "12.4") + result = windows_cuda_attempts(host, self.TAG, assets, "cuda12") + assert len(result) == 2 + assert result[0].runtime_line == "cuda12" + + def test_preferred_unavailable(self, monkeypatch): + mock_windows_runtime(monkeypatch, ["cuda12"]) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4)) + assets = self._upstream("12.4") + result = windows_cuda_attempts(host, self.TAG, assets, "cuda13") + assert len(result) == 1 + assert result[0].runtime_line == "cuda12" + + def test_detected_incompatible_with_driver(self, monkeypatch): + mock_windows_runtime(monkeypatch, ["cuda13"]) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4)) + assets = self._upstream("12.4") + result = windows_cuda_attempts(host, self.TAG, assets, None) + assert len(result) == 1 + assert result[0].runtime_line == "cuda12" + + def test_driver_too_old(self, monkeypatch): + mock_windows_runtime(monkeypatch, []) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (11, 8)) + assets = self._upstream("12.4") + result = windows_cuda_attempts(host, self.TAG, assets, None) + assert result == [] + + def test_asset_missing_from_upstream(self, monkeypatch): + mock_windows_runtime(monkeypatch, ["cuda12"]) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4)) + result = windows_cuda_attempts(host, self.TAG, {}, None) + assert result == [] + + def test_both_assets_present(self, monkeypatch): + mock_windows_runtime(monkeypatch, ["cuda13", "cuda12"]) + host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (13, 1)) + assets = self._upstream("13.1", "12.4") + result = windows_cuda_attempts(host, self.TAG, assets, None) + assert len(result) == 2 + + +# =========================================================================== +# L. resolve_upstream_asset_choice -- platform routing +# =========================================================================== + + +class TestResolveUpstreamAssetChoice: + TAG = "b8508" + + def _mock_github_assets(self, monkeypatch, assets): + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: assets, + ) + + def test_linux_x86_64_cpu(self, monkeypatch): + name = f"llama-{self.TAG}-bin-ubuntu-x64.tar.gz" + self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"}) + host = make_host( + has_usable_nvidia = False, nvidia_smi = None, has_physical_nvidia = False + ) + result = resolve_upstream_asset_choice(host, self.TAG) + assert result.install_kind == "linux-cpu" + assert result.name == name + + def test_linux_cpu_missing(self, monkeypatch): + self._mock_github_assets(monkeypatch, {}) + host = make_host( + has_usable_nvidia = False, nvidia_smi = None, has_physical_nvidia = False + ) + with pytest.raises(PrebuiltFallback, match = "Linux CPU"): + resolve_upstream_asset_choice(host, self.TAG) + + def test_windows_x86_64_cpu(self, monkeypatch): + name = f"llama-{self.TAG}-bin-win-cpu-x64.zip" + self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"}) + host = make_host( + system = "Windows", + machine = "AMD64", + has_usable_nvidia = False, + nvidia_smi = None, + has_physical_nvidia = False, + ) + result = resolve_upstream_asset_choice(host, self.TAG) + assert result.install_kind == "windows-cpu" + assert result.name == name + + def test_windows_cpu_missing(self, monkeypatch): + self._mock_github_assets(monkeypatch, {}) + host = make_host( + system = "Windows", + machine = "AMD64", + has_usable_nvidia = False, + nvidia_smi = None, + has_physical_nvidia = False, + ) + with pytest.raises(PrebuiltFallback, match = "Windows CPU"): + resolve_upstream_asset_choice(host, self.TAG) + + def test_macos_arm64(self, monkeypatch): + name = f"llama-{self.TAG}-bin-macos-arm64.tar.gz" + self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"}) + host = make_host( + system = "Darwin", + machine = "arm64", + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + result = resolve_upstream_asset_choice(host, self.TAG) + assert result.install_kind == "macos-arm64" + assert result.name == name + + def test_macos_arm64_missing(self, monkeypatch): + self._mock_github_assets(monkeypatch, {}) + host = make_host( + system = "Darwin", + machine = "arm64", + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + with pytest.raises(PrebuiltFallback, match = "macOS arm64"): + resolve_upstream_asset_choice(host, self.TAG) + + def test_macos_x86_64(self, monkeypatch): + name = f"llama-{self.TAG}-bin-macos-x64.tar.gz" + self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"}) + host = make_host( + system = "Darwin", + machine = "x86_64", + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + result = resolve_upstream_asset_choice(host, self.TAG) + assert result.install_kind == "macos-x64" + assert result.name == name + + def test_linux_aarch64(self, monkeypatch): + self._mock_github_assets(monkeypatch, {}) + host = make_host( + system = "Linux", + machine = "aarch64", + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + with pytest.raises( + PrebuiltFallback, match = "no prebuilt policy exists for Linux aarch64" + ): + resolve_upstream_asset_choice(host, self.TAG) + + def test_windows_usable_nvidia_delegates(self, monkeypatch): + cuda_name = f"llama-{self.TAG}-bin-win-cuda-12.4-x64.zip" + self._mock_github_assets(monkeypatch, {cuda_name: f"https://x/{cuda_name}"}) + mock_windows_runtime(monkeypatch, ["cuda12"]) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_windows_cuda_choices", + lambda host, tag, assets: [ + AssetChoice( + repo = UPSTREAM_REPO, + tag = tag, + name = cuda_name, + url = f"https://x/{cuda_name}", + source_label = "upstream", + install_kind = "windows-cuda", + runtime_line = "cuda12", + ) + ], + ) + host = make_host( + system = "Windows", + machine = "AMD64", + driver_cuda_version = (12, 4), + has_usable_nvidia = True, + ) + result = resolve_upstream_asset_choice(host, self.TAG) + assert result.install_kind == "windows-cuda" + assert result.name == cuda_name diff --git a/tests/utils/test_q_galore.py b/tests/utils/test_q_galore.py new file mode 100644 index 0000000000..6dea5014a0 --- /dev/null +++ b/tests/utils/test_q_galore.py @@ -0,0 +1,528 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Tests for Q-GaLore integration (unsloth/optimizers/). + +import pytest +import sys +import os +import torch +import torch.nn as nn + +# Import the optimizers module directly to avoid triggering unsloth.__init__ +# which requires unsloth_zoo and other heavy dependencies. +_repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +_optimizers_dir = os.path.join(_repo_root, "unsloth", "optimizers") +if _repo_root not in sys.path: + sys.path.insert(0, _repo_root) + +# Direct import of the actual modules (avoids unsloth/__init__.py) +import importlib.util + + +def _load_module(name, filepath): + spec = importlib.util.spec_from_file_location(name, filepath) + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +# Load projector module first (no dependencies on unsloth) +_projector_mod = _load_module( + "unsloth.optimizers.q_galore_projector", + os.path.join(_optimizers_dir, "q_galore_projector.py"), +) +GaLoreProjector = _projector_mod.GaLoreProjector +_quantize = _projector_mod._quantize +_dequantize = _projector_mod._dequantize +_quantize_stochastic = _projector_mod._quantize_stochastic + +# Load adamw module (depends on projector, may skip bitsandbytes) +_adamw_mod = _load_module( + "unsloth.optimizers.q_galore_adamw", + os.path.join(_optimizers_dir, "q_galore_adamw.py"), +) +make_q_galore_param_groups = _adamw_mod.make_q_galore_param_groups + +# ====================================================================== +# Projector tests +# ====================================================================== + + +class TestGaLoreProjector: + """Tests for the GaLore low-rank gradient projector.""" + + def test_project_and_back_tall(self): + """Project → project_back preserves shape for tall matrices.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1) + grad = torch.randn(16, 8) # tall + low = proj.project(grad, step = 0) + assert low.shape == (16, 4) + + full = proj.project_back(low) + assert full.shape == grad.shape + + def test_project_and_back_wide(self): + """Project → project_back preserves shape for wide matrices.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1) + grad = torch.randn(8, 16) # wide + low = proj.project(grad, step = 0) + assert low.shape == (4, 16) + + full = proj.project_back(low) + assert full.shape == grad.shape + + def test_project_reuses_cached_svd(self): + """SVD is not recomputed when step is not a multiple of update_proj_gap.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 100) + grad = torch.randn(16, 8) + proj.project(grad, step = 0) + assert proj.svd_count == 1 + + proj.project(grad, step = 1) + assert proj.svd_count == 1 # No recomputation + + proj.project(grad, step = 100) + assert proj.svd_count == 2 # Recomputed + + def test_quantized_projection(self): + """Quantized projection matrix stores and restores with bounded error.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, quant = True, n_bit = 8) + grad = torch.randn(16, 8) + low = proj.project(grad, step = 0) + assert low.shape == (16, 4) + + # The projection matrix should be stored as uint8 + assert proj.ortho_matrix.dtype == torch.uint8 + + def test_quantized_projection_int4(self): + """INT4 quantized projection stores correctly.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, quant = True, n_bit = 4) + grad = torch.randn(16, 8) + proj.project(grad, step = 0) + assert proj.ortho_matrix.dtype == torch.uint8 + # INT4 values should be in range [0, 15] + assert proj.ortho_matrix.max() <= 15 + + def test_adaptive_scheduling(self): + """update_proj_gap increases when cosine similarity exceeds threshold.""" + proj = GaLoreProjector( + rank = 4, + update_proj_gap = 10, + cos_threshold = 0.0, # Very low threshold → always triggers + gamma_proj = 2.0, + queue_size = 2, + ) + # Use very similar gradients so cosine similarity is high + base_grad = torch.randn(16, 8) + for i in range(5): + grad = base_grad + torch.randn_like(base_grad) * 0.001 + proj.project(grad, step = i * 10) + + # After several similar SVDs, update_proj_gap should have increased + assert proj.update_proj_gap > 10 + + def test_scale_applied(self): + """project_back applies the scale factor.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, scale = 0.5) + grad = torch.randn(16, 8) + low = proj.project(grad, step = 0) + + proj2 = GaLoreProjector(rank = 4, update_proj_gap = 1, scale = 1.0) + low2 = proj2.project(grad, step = 0) + + full_half = proj.project_back(low) + full_one = proj2.project_back(low2) + + # The ratio should be exactly 0.5 (SVD is deterministic on same input) + ratio = full_half.norm() / full_one.norm() + assert abs(ratio - 0.5) < 1e-5, f"Expected ratio ~0.5, got {ratio:.8f}" + + +# ====================================================================== +# Quantization utility tests +# ====================================================================== + + +class TestQuantizationUtils: + """Tests for _quantize, _dequantize, _quantize_stochastic.""" + + def test_quantize_dequantize_roundtrip(self): + """Quantize → dequantize has bounded error.""" + w = torch.randn(32, 64) + q, scales, zeros, shape = _quantize(w, n_bit = 8) + w_hat = _dequantize(q, scales, zeros, shape) + + # Error should be bounded by the quantization step size + error = (w - w_hat).abs().max() + assert error < 0.1, f"Max error {error} exceeds threshold" + + def test_quantize_group_roundtrip(self): + """Grouped quantization → dequantization has bounded error.""" + w = torch.randn(32, 64) + q, scales, zeros, shape = _quantize(w, q_group_size = 32, n_bit = 8) + w_hat = _dequantize(q, scales, zeros, shape) + error = (w - w_hat).abs().max() + assert error < 0.1 + + def test_quantize_dtype(self): + """Quantized output should be uint8.""" + w = torch.randn(16, 16) + q, _, _, _ = _quantize(w, n_bit = 8) + assert q.dtype == torch.uint8 + + def test_quantize_int4_range(self): + """INT4 values should be in [0, 15].""" + w = torch.randn(16, 16) + q, _, _, _ = _quantize(w, n_bit = 4) + assert q.max() <= 15 + assert q.min() >= 0 + + def test_stochastic_rounding_unbiased(self): + """Stochastic rounding should be approximately unbiased.""" + torch.manual_seed(42) + w = torch.randn(64, 64) + errors = [] + for _ in range(50): + q, scales, zeros, shape = _quantize_stochastic(w, n_bit = 8) + w_hat = _dequantize(q, scales, zeros, shape) + errors.append((w - w_hat).mean().item()) + + mean_error = sum(errors) / len(errors) + assert ( + abs(mean_error) < 0.01 + ), f"Mean error {mean_error} suggests biased rounding" + + +# ====================================================================== +# Param group helper tests +# ====================================================================== + + +class TestParamGroupHelper: + """Tests for make_q_galore_param_groups.""" + + def test_param_group_separation(self): + """GaLore vs non-GaLore params are correctly separated.""" + + # Create a mini-transformer-like model + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + model.k_proj = nn.Linear(64, 64, bias = False) + model.embed = nn.Embedding(100, 64) + model.norm = nn.LayerNorm(64) + + groups = make_q_galore_param_groups(model, rank = 8, weight_quant = False) + + # Should have 2 groups: galore and non-galore + assert len(groups) == 2 + + galore_group = [g for g in groups if "rank" in g][0] + non_galore_group = [g for g in groups if "rank" not in g][0] + + # q_proj and k_proj should be in galore group (2 params) + assert len(galore_group["params"]) == 2 + # embed and norm should be in non-galore group + assert ( + len(non_galore_group["params"]) == 3 + ) # embed weight + norm weight + norm bias + + def test_custom_target_modules(self): + """Custom target_modules narrows GaLore scope.""" + + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + model.k_proj = nn.Linear(64, 64, bias = False) + model.v_proj = nn.Linear(64, 64, bias = False) + model.embed = nn.Embedding(100, 64) + + groups = make_q_galore_param_groups( + model, + rank = 8, + target_modules = ["q_proj"], + weight_quant = False, + ) + + galore_group = [g for g in groups if "rank" in g][0] + assert len(galore_group["params"]) == 1 # Only q_proj + + def test_bias_excluded_from_galore(self): + """1D bias params matching target names must NOT be in the GaLore group. + + GaLoreProjector.project requires 2-D gradients, so bias vectors + (e.g. q_proj.bias) that match a target name must be excluded. + """ + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = True) # has .weight AND .bias + model.embed = nn.Embedding(100, 64) + + groups = make_q_galore_param_groups(model, rank = 8, weight_quant = False) + + galore_group = [g for g in groups if "rank" in g][0] + non_galore_group = [g for g in groups if "rank" not in g][0] + + # Only the 2-D q_proj.weight should be in the GaLore group + assert len(galore_group["params"]) == 1 + assert galore_group["params"][0].dim() == 2 + + # q_proj.bias (1-D) + embed.weight should be in non-GaLore + assert any(p.dim() == 1 for p in non_galore_group["params"]) + + def test_empty_target_modules_no_galore(self): + """target_modules=[] should result in no GaLore params.""" + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + + # Pass empty list, should NOT fall back to defaults + groups = make_q_galore_param_groups( + model, + rank = 8, + target_modules = [], + weight_quant = False, + ) + + galore_groups = [g for g in groups if "rank" in g] + assert ( + len(galore_groups) == 0 + ), "Expected no GaLore groups when target_modules=[]" + + +# ====================================================================== +# Optimizer tests (CPU-only, no bitsandbytes dependency) +# ====================================================================== + + +class TestQGaLoreIntegration: + """Integration tests that work without bitsandbytes on CPU.""" + + def test_projector_training_loop(self): + """A simple training loop using manual GaLore projection converges.""" + torch.manual_seed(42) + + # Tiny model: single linear layer + model = nn.Linear(32, 16, bias = False) + target = torch.randn(4, 16) + x = torch.randn(4, 32) + + proj = GaLoreProjector(rank = 8, update_proj_gap = 1, scale = 1.0) + optimizer = torch.optim.AdamW(model.parameters(), lr = 0.01) + + losses = [] + for step in range(20): + optimizer.zero_grad() + out = model(x) + loss = nn.functional.mse_loss(out, target) + loss.backward() + losses.append(loss.item()) + + # Manual GaLore projection + for p in model.parameters(): + if p.grad is not None and p.grad.dim() == 2: + low = proj.project(p.grad, step) + p._saved = p.data.clone() + update = torch.zeros_like(low) + update.add_(low) # Simplified update + full_update = proj.project_back(update) + p.grad.copy_(full_update) + + optimizer.step() + + # Loss should decrease + assert ( + losses[-1] < losses[0] + ), f"Loss did not decrease: {losses[0]:.4f} → {losses[-1]:.4f}" + + def test_full_projector_roundtrip_quality(self): + """project → project_back captures the dominant gradient directions.""" + torch.manual_seed(42) + # Create a gradient with clear low-rank structure + u = torch.randn(32, 4) + v = torch.randn(4, 16) + grad = u @ v # rank-4 gradient + + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, scale = 1.0) + low = proj.project(grad, step = 0) + reconstructed = proj.project_back(low) + + # For a rank-4 gradient with rank-4 projection, reconstruction + # should be very close to original + relative_error = (grad - reconstructed).norm() / grad.norm() + assert ( + relative_error < 0.05 + ), f"Reconstruction error too high: {relative_error:.4f}" + + def test_weight_quant_activates_on_first_step(self): + """_has_weight_quant returns True even when _q_scales is None (first step).""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + QGaLoreAdamW8bit = _adamw_mod_local.QGaLoreAdamW8bit + + p = torch.nn.Parameter(torch.randn(16, 16)) + # Simulate init_weight_quantization tagging + p._q_scales = None + p._q_zeros = None + p._q_shape = p.data.shape + + group = {"weight_quant": True} + + # _has_weight_quant must return True even on first step (_q_scales=None) + assert QGaLoreAdamW8bit._has_weight_quant(p, group) is True + + # Without the tag, it should return False + p2 = torch.nn.Parameter(torch.randn(16, 16)) + assert QGaLoreAdamW8bit._has_weight_quant(p2, group) is False + + def test_embedding_lr_param_group_split(self): + """Embedding params can be split into a separate group with custom LR.""" + # This tests the logic that make_q_galore_param_groups produces groups + # that can be further split by the trainer for embedding LR. + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + model.embed = nn.Embedding(100, 64) + + groups = make_q_galore_param_groups(model, rank = 8, weight_quant = False) + + # Simulate splitting non-GaLore group for embedding LR + embed_lr = 5e-5 + new_groups = [] + for group in groups: + if "rank" in group: + new_groups.append(group) + continue + embed_params = [] + other_params = [] + for p in group["params"]: + # In real usage, we'd check the name; here just split by shape + if p.shape[0] == 100: # embedding + embed_params.append(p) + else: + other_params.append(p) + if other_params: + g = dict(group) + g["params"] = other_params + new_groups.append(g) + if embed_params: + g = dict(group) + g["params"] = embed_params + g["lr"] = embed_lr + new_groups.append(g) + + # Should have 3 groups: galore, non-galore non-embed, embed + embed_groups = [g for g in new_groups if g.get("lr") == embed_lr] + assert len(embed_groups) == 1 + assert embed_groups[0]["lr"] == embed_lr + + def test_optimizer_hyperparams_forwarded(self): + """QGaLoreAdamW8bit accepts betas and eps keyword arguments.""" + # Verify the constructor signature accepts these params. + # Without bitsandbytes we can't instantiate, but we can check the + # function signature. + import inspect + + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + QGaLoreAdamW8bit = _adamw_mod_local.QGaLoreAdamW8bit + + sig = inspect.signature(QGaLoreAdamW8bit.__init__) + param_names = list(sig.parameters.keys()) + assert "betas" in param_names, "betas not in QGaLoreAdamW8bit.__init__ params" + assert "eps" in param_names, "eps not in QGaLoreAdamW8bit.__init__ params" + + def test_weight_decay_uses_saved_data(self): + """Weight decay should apply standard decoupled AdamW decay on current weights.""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + + # Create a mock parameter and group + p = torch.nn.Parameter(torch.ones(4, 4)) + p._saved_data = torch.ones(4, 4) * 2.0 # Pre-update weights + # Simulate project-back: p.data = p._saved_data + projected update + p.data = p._saved_data.add_(torch.ones(4, 4) * 1.0) # p.data is now 3.0 + + group = {"weight_decay": 0.1, "lr": 1.0, "_wd_saved": 0.1} + + # Replicate the fixed decoupled weight decay logic (uses p.data, not p._saved_data) + p.data.add_( + p.data, + alpha = -group["lr"] * group["_wd_saved"], + ) + + del p._saved_data # Clean up after all uses, matching fixed code + + # Decoupled weight decay: 3.0 - (1.0 * 0.1 * 3.0) = 2.7 + assert torch.allclose( + p.data, torch.tensor(2.7) + ), "Weight decay didn't use p.data for decoupled decay!" + + def test_params_float_after_weight_quant_step(self): + """After a step with weight_quant=True, parameters must remain floating point.""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + _projector_mod_local = sys.modules["unsloth.optimizers.q_galore_projector"] + + _quantize = _projector_mod_local._quantize + + p = torch.nn.Parameter(torch.randn(16, 16)) + group = { + "weight_quant": True, + "stochastic_round": False, + "weight_group_size": 16, + } + + # Replicate the re-quantize logic at the end of optimizer step + float_data = p.data.clone() + q, scales, zeros, shape = _quantize( + float_data, q_group_size = group["weight_group_size"] + ) + + # The key assertion: p.data stays float, _q_data holds uint8 + p._q_data = q.to(p.data.device) + p._q_scales = scales + p._q_zeros = zeros + p._q_shape = shape + + assert p.data.is_floating_point(), "p.data was converted to uint8!" + assert p._q_data.dtype == torch.uint8, "_q_data should be uint8!" + + def test_weight_quant_hook_restores_float(self): + """Forward pre-hook should dequantize INT8 weights before forward pass.""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + _projector_mod_local = sys.modules["unsloth.optimizers.q_galore_projector"] + install_hook = _adamw_mod_local.install_weight_quant_hooks + + linear = nn.Linear(16, 8, bias = False) + original = linear.weight.data.clone() + + # Quantize the weight and replace with placeholder (simulates post-step) + q, scales, zeros, shape = _projector_mod_local._quantize( + linear.weight.data.clone(), q_group_size = 16 + ) + linear.weight._q_data = q + linear.weight._q_scales = scales + linear.weight._q_zeros = zeros + linear.weight._q_shape = shape + linear.weight.data = torch.zeros(1, dtype = linear.weight.dtype) + assert linear.weight.data.numel() == 1, "placeholder should be 1 element" + + # Install hook and run forward -- should restore float weights + handles = install_hook(linear) + x = torch.randn(2, 16) + out = linear(x) # triggers pre-hook + + assert linear.weight.data.shape == (8, 16), "weight shape not restored" + assert linear.weight.data.is_floating_point(), "weight not float after hook" + # Check values are close to original (quantization introduces small error) + assert torch.allclose( + linear.weight.data, original, atol = 0.15 + ), "dequantized weight too far from original" + + for h in handles: + h.remove() diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 13acc98ea6..02e2170b70 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2026.3.11" +__version__ = "2026.3.12" __all__ = [ "SUPPORTS_BFLOAT16", diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index a05186eee5..581244e4d3 100755 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -93,6 +93,58 @@ def vLLMSamplingParams(**kwargs): return sampling_params +def _maybe_prepare_vllm_for_resume(trainer): + if not torch.cuda.is_available(): + return + + llm = getattr(trainer, "llm", None) + if llm is None: + llm = getattr(getattr(trainer, "model", None), "vllm_engine", None) + if llm is None: + return + + model_config = getattr( + getattr(getattr(llm, "llm_engine", None), "vllm_config", None), + "model_config", + None, + ) + if not getattr(model_config, "enable_sleep_mode", False): + return + + try: + llm.sleep(1) + except Exception: + pass + + import gc + + for _ in range(3): + gc.collect() + torch.cuda.empty_cache() + + +def _patch_resume_from_checkpoint_memory(trainer_class): + original_train = getattr(trainer_class, "train", None) + if original_train is None: + return + if getattr(original_train, "_unsloth_resume_guard", False): + return + + def _unsloth_train_with_resume_guard(self, *args, **kwargs): + resume_from_checkpoint = kwargs.get("resume_from_checkpoint", None) + if resume_from_checkpoint is None: + resume_from_checkpoint = kwargs.get("model_path", None) + if resume_from_checkpoint is None and len(args) != 0: + resume_from_checkpoint = args[0] + + if resume_from_checkpoint: + _maybe_prepare_vllm_for_resume(self) + return original_train(self, *args, **kwargs) + + _unsloth_train_with_resume_guard._unsloth_resume_guard = True + trainer_class.train = _unsloth_train_with_resume_guard + + def PatchRL(FastLanguageModel): try: from trl.models.utils import unwrap_model_for_generation @@ -305,7 +357,6 @@ from transformers.training_args import ParallelMode from unsloth_zoo.device_type import DEVICE_TYPE, device_synchronize # Wrap trainer with padding to right and enable training mode -# Also patches W&B since multiple runs must use wandb.finish() import functools from types import MethodType try: @@ -315,6 +366,23 @@ except: def prepare_for_training_mode(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): + # Finish the previous W&B run if this is a subsequent train() call. + # We do this at the START of train() (not the end) so that + # evaluate() / log() still work after train() completes. + # HF's WandbCallback.setup() will call wandb.init() for the new run. + # See: https://github.com/unslothai/unsloth/issues/3954 + if getattr(self, '_unsloth_training_completed', False): + try: + import wandb + if wandb.run is not None: + wandb.finish() + # Reset HF's WandbCallback so it calls wandb.init() for the new run + for cb in self.callback_handler.callbacks: + if type(cb).__name__ == 'WandbCallback': + cb._initialized = False + break + except: + pass # Enable training mode _was_training = None # Get gradient checkpointing setting from training arguments @@ -335,12 +403,9 @@ def prepare_for_training_mode(f): reset_unsloth_gradient_checkpointing_buffers() except: pass - # Patch W&B to enable logging on future runs, otherwise it'll overwrite the first run - try: - import wandb - wandb.finish() - except: - pass + # Mark that training completed so the next train() call can + # finish this W&B run before starting a new one + self._unsloth_training_completed = True return output return wrapper pass @@ -686,8 +751,8 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): else: continue call_args.append(f"{k} = {k}") - arguments = f"\n{' '*8}" + f",\n{' '*8}".join(arguments) - call_args = f"\n{' '*12}" + f",\n{' '*12}".join(call_args) + arguments = f"\n{' ' * 8}" + f",\n{' ' * 8}".join(arguments) + call_args = f"\n{' ' * 12}" + f",\n{' ' * 12}".join(call_args) processed.append( ( arguments, @@ -701,7 +766,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): # Add tokenizer if not seen if "tokenizer" not in parameters and "processing_class" in parameters: - arguments += f",\n{' '*8}tokenizer = None" + arguments += f",\n{' ' * 8}tokenizer = None" call_args = call_args.replace( "processing_class = processing_class", "processing_class = tokenizer if tokenizer is not None else processing_class", @@ -1490,6 +1555,9 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): imports, overwrite = False, ) + patched_trainer = getattr(created_module, f"Unsloth{RLTrainer_name}") + if trainer_file == "grpo_trainer": + _patch_resume_from_checkpoint_memory(patched_trainer) # Patch Trainer exec( @@ -1706,8 +1774,8 @@ def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, import sampling_params = re.sub(r"[\,][\s]{0,}\,", ",", sampling_params) new_vllm_part = ( - f"\n{' '*8}if {args}.use_vllm:\n{sampling_params}" - f"\n{' '*8}else:\n" + f"\n{' ' * 8}if {args}.use_vllm:\n{sampling_params}" + f"\n{' ' * 8}else:\n" ) if trl_version >= Version("0.18.0"): diff --git a/unsloth/optimizers/__init__.py b/unsloth/optimizers/__init__.py new file mode 100644 index 0000000000..b126321ab1 --- /dev/null +++ b/unsloth/optimizers/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .q_galore_projector import GaLoreProjector +from .q_galore_adamw import QGaLoreAdamW8bit + +__all__ = [ + "GaLoreProjector", + "QGaLoreAdamW8bit", +] diff --git a/unsloth/optimizers/q_galore_adamw.py b/unsloth/optimizers/q_galore_adamw.py new file mode 100644 index 0000000000..6cd0a4a846 --- /dev/null +++ b/unsloth/optimizers/q_galore_adamw.py @@ -0,0 +1,424 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Adapted from Q-GaLore (https://github.com/VITA-Group/Q-GaLore) +# Original paper: "Q-GaLore: Quantized GaLore with INT4 Projection and +# Layer-Adaptive Low-Rank Gradients" (arXiv:2407.08296) + +import torch +from typing import Optional, List + +from .q_galore_projector import ( + GaLoreProjector, + _quantize, + _quantize_stochastic, + _dequantize, +) + +__all__ = ["QGaLoreAdamW8bit", "install_weight_quant_hooks"] + +try: + import bitsandbytes.functional as bnb_F + from bitsandbytes.optim.optimizer import Optimizer2State + + _HAS_BNB = True +except ImportError: + _HAS_BNB = False + # Provide a fallback base so the module can at least be imported. + Optimizer2State = torch.optim.Optimizer + + +def _require_bnb(): + if not _HAS_BNB: + raise ImportError( + "Unsloth: Q-GaLore requires bitsandbytes. " + "Install it with: pip install bitsandbytes" + ) + + +class QGaLoreAdamW8bit(Optimizer2State): + """AdamW optimizer with 8-bit states, GaLore low-rank gradient projection, + and optional INT8 weight quantization. + + This optimizer combines three memory-saving techniques: + + 1. **8-bit optimizer states** (via bitsandbytes) — Adam's first and second + moments are stored in 8-bit, reducing optimizer state memory by ~4×. + + 2. **GaLore low-rank gradient projection** — gradients are projected into a + low-rank subspace before the optimizer step, then projected back. The + projection matrix itself can be quantized to INT4. + + 3. **INT8 weight quantization** — model weights are stored in INT8 during + training with stochastic rounding, reducing weight memory by ~2× for + eligible layers. + + Param group keys consumed by GaLore projection: + ``rank``, ``update_proj_gap``, ``scale``, ``proj_type``, + ``quant`` (projection quantization), ``quant_group_size``, + ``quant_n_bit``, ``cos_threshold``, ``gamma_proj``, ``queue_size`` + + Param group keys for weight quantization: + ``weight_quant``, ``stochastic_round``, ``weight_group_size`` + """ + + def __init__( + self, + params, + lr: float = 1e-3, + betas: tuple = (0.9, 0.999), + eps: float = 1e-8, + weight_decay: float = 1e-2, + min_8bit_size: int = 4096, + percentile_clipping: int = 100, + block_wise: bool = True, + is_paged: bool = False, + ): + _require_bnb() + super().__init__( + "adam", + params, + lr, + betas, + eps, + weight_decay, + 8, # optim_bits + None, # args + min_8bit_size, + percentile_clipping, + block_wise, + is_paged = is_paged, + ) + + # ------------------------------------------------------------------ + # Core step + # ------------------------------------------------------------------ + + @torch.no_grad() + def step(self, closure = None): + """Perform a single optimization step. + + For each parameter that has a ``rank`` key in its param group, the + following sequence is executed: + + 1. If ``weight_quant`` is set, dequantize the INT8 weight to float. + 2. Project the gradient to low-rank via the cached ``GaLoreProjector``. + 3. Perform the 8-bit Adam update in the low-rank space. + 4. Project the update back to full rank and add to saved weight. + 5. If ``weight_quant`` is set, re-quantize the weight to INT8. + """ + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + if not self.initialized: + self.check_overrides() + self.to_gpu() + self.initialized = True + + for gindex, group in enumerate(self.param_groups): + for pindex, p in enumerate(group["params"]): + if p.grad is None: + continue + + state = self.state[p] + if "step" not in state: + state["step"] = 0 + + has_weight_quant = self._has_weight_quant(p, group) + + # --- Dequantize weight if INT8 --- + if has_weight_quant: + if p._q_scales is not None: + float_weight = _dequantize( + p._q_data, + p._q_scales, + p._q_zeros, + p._q_shape, + ) + p.data = float_weight + # else: first step, weights are still float — skip dequantize + + # --- GaLore projection --- + if "rank" in group: + if "projector" not in state: + state["projector"] = GaLoreProjector( + rank = group["rank"], + update_proj_gap = group.get("update_proj_gap", 200), + scale = group.get("scale", 0.25), + proj_type = group.get("proj_type", "std"), + quant = group.get("quant", False), + group_size = group.get("quant_group_size", -1), + n_bit = group.get("quant_n_bit", 4), + cos_threshold = group.get("cos_threshold", 0.4), + gamma_proj = group.get("gamma_proj", 2.0), + queue_size = group.get("queue_size", 5), + ) + + # Temporarily disable weight decay for GaLore params + # (we apply it manually after project-back) + if "weight_decay" in group and group["weight_decay"] > 0: + group["_wd_saved"] = group["weight_decay"] + group["weight_decay"] = 0 + + grad = state["projector"].project(p.grad, state["step"]) + + # Save current weight; replace p.data with zeros so + # the 8-bit update writes the pure weight delta. + p._saved_data = p.data.clone() + p.data = torch.zeros_like( + grad, dtype = p.data.dtype, device = p.data.device + ) + p.grad = grad + + # --- 8-bit Adam update --- + if "state1" not in state: + self.init_state(group, p, gindex, pindex) + + self.prefetch_state(p) + self.update_step(group, p, gindex, pindex) + + # --- GaLore project-back --- + if "rank" in group: + # p.data now holds the weight update in low-rank space + p.data = p._saved_data.add_(state["projector"].project_back(p.data)) + + # Re-apply decoupled weight decay using pre-update weights + if "_wd_saved" in group: + p.data.add_( + p.data, + alpha = -group["lr"] * group["_wd_saved"], + ) + group["weight_decay"] = group["_wd_saved"] + del group["_wd_saved"] + + del p._saved_data + + # --- Re-quantize weight to INT8 --- + if has_weight_quant: + float_data = p.data + stochastic = group.get("stochastic_round", True) + gsize = group.get("weight_group_size", 128) + quant_fn = _quantize_stochastic if stochastic else _quantize + q, scales, zeros, shape = quant_fn(float_data, q_group_size = gsize) + p._q_data = q.to(p.data.device) + p._q_scales = scales + p._q_zeros = zeros + p._q_shape = shape + # Replace p.data with a scalar placeholder to free float memory. + # A forward pre-hook (install_weight_quant_hooks) will + # dequantize back to float before the next forward pass. + p.data = torch.empty(1, dtype = p.data.dtype, device = p.data.device) + + state["step"] += 1 + + if torch.cuda.is_available(): + torch.cuda.synchronize() + + return loss + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _has_weight_quant(p: torch.Tensor, group: dict) -> bool: + """Check if this parameter uses INT8 weight quantization.""" + return ( + group.get("weight_quant", False) + and hasattr(p, "_q_scales") # tag set by init_weight_quantization() + ) + + @staticmethod + def init_weight_quantization( + model: torch.nn.Module, + param_groups: list, + group_size: int = 128, + stochastic: bool = True, + ) -> None: + """Tag parameters for INT8 weight quantization. + + This marks eligible weights with quantization metadata so that + the optimizer knows to quantize/dequantize them during ``step()``. + **Weights are NOT converted to uint8 here** — they remain in float + so that the first forward/backward pass runs correctly. The actual + quantization happens at the end of the first ``step()`` call. + """ + weight_quant_params = set() + for group in param_groups: + if group.get("weight_quant", False): + for p in group["params"]: + weight_quant_params.add(id(p)) + + for name, p in model.named_parameters(): + if id(p) in weight_quant_params: + # Store quantization metadata WITHOUT converting weights to + # uint8. The first optimizer.step() will quantize after the + # update. We store dummy scales/zeros so _has_weight_quant() + # returns True on the first step. + p._q_scales = None + p._q_zeros = None + p._q_shape = p.data.shape + p._stochastic_round = stochastic + p._weight_group_size = group_size + + +def _weight_quant_pre_hook(module, args): + """Forward pre-hook: dequantize INT8 weights to float before forward.""" + for p in module.parameters(recurse = False): + if hasattr(p, "_q_scales") and p._q_scales is not None: + float_weight = _dequantize( + p._q_data, + p._q_scales, + p._q_zeros, + p._q_shape, + ) + p.data = float_weight.to(p.data.device) + + +def install_weight_quant_hooks(model: torch.nn.Module) -> list: + """Register forward pre-hooks on modules whose weights are INT8-quantized. + + Returns a list of hook handles so the caller can remove them if needed. + """ + handles = [] + for module in model.modules(): + has_quant_param = any( + hasattr(p, "_q_scales") for p in module.parameters(recurse = False) + ) + if has_quant_param: + h = module.register_forward_pre_hook(_weight_quant_pre_hook) + handles.append(h) + return handles + + +# ====================================================================== +# Param-group construction helper +# ====================================================================== + +# Default linear layer names in transformer blocks that should use GaLore. +_DEFAULT_GALORE_TARGETS = { + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", +} + + +def make_q_galore_param_groups( + model: torch.nn.Module, + lr: float = 1e-3, + weight_decay: float = 0.0, + rank: int = 256, + update_proj_gap: int = 200, + scale: float = 0.25, + proj_quant: bool = True, + proj_quant_group_size: int = -1, + proj_quant_n_bit: int = 4, + weight_quant: bool = False, + stochastic_round: bool = True, + weight_group_size: int = 128, + cos_threshold: float = 0.4, + gamma_proj: float = 2.0, + queue_size: int = 5, + target_modules: Optional[List[str]] = None, +) -> list: + """Build param groups suitable for :class:`QGaLoreAdamW8bit`. + + Parameters matching ``target_modules`` (or the default set of attention + and MLP projection names) are placed in the GaLore group. All other + trainable parameters go into the non-GaLore group. + + Args: + model: The model whose parameters to partition. + lr: Learning rate for all parameter groups. + weight_decay: Weight decay coefficient. + rank: GaLore projection rank. + update_proj_gap: Steps between SVD recomputations. + scale: Scaling factor for project-back. + proj_quant: Quantize projection matrices. + proj_quant_group_size: Group size for projection quantization. + proj_quant_n_bit: Bit-width for projection quantization. + weight_quant: Enable INT8 weight quantization for GaLore params. + stochastic_round: Use stochastic rounding for weight quantization. + weight_group_size: Group size for weight quantization. + cos_threshold: Cosine similarity threshold for adaptive scheduling. + gamma_proj: Multiplier for update_proj_gap when subspace is stable. + queue_size: Rolling window size for stability tracking. + target_modules: Module name substrings to match for GaLore. If None, + uses the default set of attention/MLP projection names. + + Returns: + List of two param group dicts: ``[galore_group, non_galore_group]``. + """ + targets = ( + set(target_modules) if target_modules is not None else _DEFAULT_GALORE_TARGETS + ) + + galore_params = [] + non_galore_params = [] + + for name, param in model.named_parameters(): + if not param.requires_grad: + continue + + # Check if any target module name appears as a component in the param name. + # Exclude 1-D parameters (biases, norms) because GaLoreProjector.project + # requires 2-D gradients. + name_parts = name.split(".") + is_galore = param.dim() >= 2 and any(t in name_parts for t in targets) + + if is_galore: + galore_params.append(param) + else: + non_galore_params.append(param) + + groups = [] + + if galore_params: + groups.append( + { + "params": galore_params, + "lr": lr, + "weight_decay": weight_decay, + "rank": rank, + "update_proj_gap": update_proj_gap, + "scale": scale, + "proj_type": "std", + "quant": proj_quant, + "quant_group_size": proj_quant_group_size, + "quant_n_bit": proj_quant_n_bit, + "weight_quant": weight_quant, + "stochastic_round": stochastic_round, + "weight_group_size": weight_group_size, + "cos_threshold": cos_threshold, + "gamma_proj": gamma_proj, + "queue_size": queue_size, + } + ) + + if non_galore_params: + groups.append( + { + "params": non_galore_params, + "lr": lr, + "weight_decay": weight_decay, + } + ) + + return groups diff --git a/unsloth/optimizers/q_galore_projector.py b/unsloth/optimizers/q_galore_projector.py new file mode 100644 index 0000000000..cabd228b92 --- /dev/null +++ b/unsloth/optimizers/q_galore_projector.py @@ -0,0 +1,385 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Adapted from Q-GaLore (https://github.com/VITA-Group/Q-GaLore) +# Original paper: "Q-GaLore: Quantized GaLore with INT4 Projection and +# Layer-Adaptive Low-Rank Gradients" (arXiv:2407.08296) + +from collections import deque + +import torch + +__all__ = ["GaLoreProjector"] + + +class GaLoreProjector: + """Low-rank gradient projector with optional INT4/INT8 quantized projection + matrices and layer-adaptive subspace update scheduling. + + The projector computes an SVD of the gradient to obtain an orthogonal basis + for the top-``rank`` subspace. Gradients are projected into this subspace + for the optimizer step, then projected back to full rank for the weight + update. + + Two key Q-GaLore innovations are implemented: + + 1. **Quantized projection matrices** — when ``quant=True``, the orthogonal + matrix is stored in INT4/INT8, reducing the memory cost of keeping the + projector state. + + 2. **Layer-adaptive update scheduling** — a rolling queue of cosine + similarities between consecutive orthogonal vectors is maintained. When + the average exceeds ``cos_threshold``, ``update_proj_gap`` is multiplied + by ``gamma_proj``, effectively reducing the frequency of expensive SVD + recomputations for layers whose subspace has stabilized. + + Args: + rank: Target rank for the low-rank projection. + update_proj_gap: Number of steps between SVD recomputations. + scale: Scaling factor applied when projecting back to full rank. + proj_type: Projection type. Only ``'std'`` is supported. + quant: Whether to quantize the projection matrix. + group_size: Group size for projection matrix quantization. + n_bit: Bit-width for projection matrix quantization (4 or 8). + cos_threshold: Cosine similarity threshold for adaptive scheduling. + gamma_proj: Multiplier for ``update_proj_gap`` on stability detection. + queue_size: Number of recent cosine similarities to average. + """ + + __slots__ = ( + "rank", + "update_proj_gap", + "scale", + "proj_type", + "quant", + "quant_group_size", + "quant_n_bit", + "cos_threshold", + "gamma_proj", + "queue_size", + "ortho_matrix", + "ortho_matrix_scales", + "ortho_matrix_zeros", + "ortho_matrix_shape", + "past_ortho_vector", + "queue", + "svd_count", + "_ortho_float_cache", + ) + + def __init__( + self, + rank: int, + update_proj_gap: int = 200, + scale: float = 1.0, + proj_type: str = "std", + quant: bool = False, + group_size: int = -1, + n_bit: int = 4, + cos_threshold: float = 0.4, + gamma_proj: float = 2.0, + queue_size: int = 5, + ): + self.rank = rank + self.update_proj_gap = update_proj_gap + self.scale = scale + self.proj_type = proj_type + + # Quantization settings for the projection matrix + self.quant = quant + self.quant_group_size = group_size + self.quant_n_bit = n_bit + + # Adaptive update scheduling state + self.cos_threshold = cos_threshold + self.gamma_proj = gamma_proj + self.queue_size = queue_size + self.past_ortho_vector = None + self.queue = deque(maxlen = queue_size) + self.svd_count = 0 + self._ortho_float_cache = None + + # Projection matrix state + self.ortho_matrix = None + self.ortho_matrix_scales = None + self.ortho_matrix_zeros = None + self.ortho_matrix_shape = None + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def project(self, full_rank_grad: torch.Tensor, step: int) -> torch.Tensor: + """Project a full-rank gradient into the low-rank subspace. + + The SVD is recomputed every ``update_proj_gap`` steps (subject to + adaptive scheduling). Between recomputations the cached orthogonal + matrix is reused. + + Args: + full_rank_grad: The full-rank gradient tensor (2-D). + step: The current optimizer step (0-indexed). + + Returns: + The low-rank gradient tensor. + """ + assert self.proj_type == "std", "Only proj_type='std' is supported." + + if full_rank_grad.shape[0] >= full_rank_grad.shape[1]: + # "tall" matrix → right projection (grad @ Q^T) + if self.ortho_matrix is None or step % self.update_proj_gap == 0: + float_ortho = self._compute_orthogonal( + full_rank_grad, + self.rank, + side = "right", + ) + self._update_adaptive_schedule(float_ortho, side = "right") + self._store_ortho(float_ortho) + + self._ortho_float_cache = self._load_ortho() + low_rank_grad = torch.matmul(full_rank_grad, self._ortho_float_cache.t()) + else: + # "wide" matrix → left projection (Q^T @ grad) + if self.ortho_matrix is None or step % self.update_proj_gap == 0: + float_ortho = self._compute_orthogonal( + full_rank_grad, + self.rank, + side = "left", + ) + self._update_adaptive_schedule(float_ortho, side = "left") + self._store_ortho(float_ortho) + + self._ortho_float_cache = self._load_ortho() + low_rank_grad = torch.matmul(self._ortho_float_cache.t(), full_rank_grad) + + return low_rank_grad + + def project_back(self, low_rank_grad: torch.Tensor) -> torch.Tensor: + """Project a low-rank update back to full rank. + + Args: + low_rank_grad: The low-rank gradient/update tensor. + + Returns: + The full-rank update scaled by ``self.scale``. + """ + float_ortho = self._ortho_float_cache + self._ortho_float_cache = None + if float_ortho is None: + float_ortho = self._load_ortho() + + if low_rank_grad.shape[0] >= low_rank_grad.shape[1]: + full_rank_grad = torch.matmul(low_rank_grad, float_ortho) + else: + full_rank_grad = torch.matmul(float_ortho, low_rank_grad) + + return full_rank_grad * self.scale + + # ------------------------------------------------------------------ + # SVD + # ------------------------------------------------------------------ + + @staticmethod + def _compute_orthogonal( + weights: torch.Tensor, + rank: int, + side: str, + ) -> torch.Tensor: + """Compute the top-``rank`` orthogonal matrix via truncated SVD. + + Args: + weights: 2-D tensor (typically the gradient). + rank: Number of singular vectors to keep. + side: ``'left'`` returns U[:, :rank], ``'right'`` returns Vh[:rank, :]. + + Returns: + Orthogonal matrix of shape ``(rank, N)`` (right) or ``(M, rank)`` (left). + """ + original_dtype = weights.dtype + original_device = weights.device + + matrix = weights.float() if original_dtype != torch.float32 else weights + + if side not in ("right", "left"): + raise ValueError(f"side must be 'left' or 'right', got '{side}'") + + m, n = matrix.shape + if min(m, n) <= rank * 2: + U, s, Vh = torch.linalg.svd(matrix, full_matrices = False) + result = Vh[:rank, :] if side == "right" else U[:, :rank] + else: + # Oversampling p=10 per Halko et al. 2009 (arXiv:0909.4061) + # recommendation of p=5..10 for large low-rank matrices. + q = min(rank + 10, min(m, n)) + U, s, V = torch.svd_lowrank(matrix, q = q, niter = 2) + result = V[:, :rank].t() if side == "right" else U[:, :rank] + + if original_dtype != torch.float32: + result = result.to(device = original_device, dtype = original_dtype) + return result + + # ------------------------------------------------------------------ + # Adaptive scheduling + # ------------------------------------------------------------------ + + def _update_adaptive_schedule( + self, + float_ortho: torch.Tensor, + side: str, + ) -> None: + """Track subspace stability and increase ``update_proj_gap`` if stable.""" + self.svd_count += 1 + + if side == "right": + current_vector = float_ortho[:1, :].flatten() + else: + current_vector = float_ortho[:, :1].flatten() + + if self.past_ortho_vector is not None: + cos_sim = torch.dot(self.past_ortho_vector, current_vector).item() + + self.queue.append(cos_sim) + + if ( + len(self.queue) == self.queue.maxlen + and sum(self.queue) / len(self.queue) >= self.cos_threshold + ): + self.update_proj_gap = int(self.update_proj_gap * self.gamma_proj) + + self.past_ortho_vector = current_vector.clone() + + # ------------------------------------------------------------------ + # Quantized projection matrix storage + # ------------------------------------------------------------------ + + def _store_ortho(self, float_ortho: torch.Tensor) -> None: + """Store the orthogonal matrix, optionally quantized.""" + if self.quant: + q, scales, zeros, shape = _quantize( + float_ortho, + q_group_size = self.quant_group_size, + n_bit = self.quant_n_bit, + ) + self.ortho_matrix = q + self.ortho_matrix_scales = scales + self.ortho_matrix_zeros = zeros + self.ortho_matrix_shape = shape + else: + self.ortho_matrix = float_ortho + + def _load_ortho(self) -> torch.Tensor: + """Load the orthogonal matrix, dequantizing if necessary.""" + if self.quant: + return _dequantize( + self.ortho_matrix, + self.ortho_matrix_scales, + self.ortho_matrix_zeros, + self.ortho_matrix_shape, + ) + return self.ortho_matrix + + +# ====================================================================== +# Quantization utilities (shared with the optimizer) +# ====================================================================== + + +@torch.no_grad() +def _quantize( + w: torch.Tensor, + q_group_size: int = -1, + n_bit: int = 8, +) -> tuple: + """Asymmetric min-max quantization to unsigned int. + + Returns: + ``(quantized_uint8, scales, zeros, original_shape)`` + """ + org_shape = w.shape + if q_group_size > 0: + assert ( + w.nelement() % q_group_size == 0 + ), f"Tensor size {w.nelement()} not divisible by group_size {q_group_size}" + w = w.reshape(-1, q_group_size) + assert w.dim() == 2 + + max_val = w.amax(dim = 1, keepdim = True) + min_val = w.amin(dim = 1, keepdim = True) + max_int = 2**n_bit - 1 + min_int = 0 + scales = (max_val - min_val).clamp(min = 1e-5) / max_int + zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int) + + w = torch.clamp(torch.round(w / scales) + zeros, min_int, max_int) + w = w.reshape(org_shape).to(torch.uint8) + + return w, scales, zeros, org_shape + + +@torch.no_grad() +def _dequantize( + w: torch.Tensor, + scales: torch.Tensor, + zeros: torch.Tensor, + original_shape: tuple, +) -> torch.Tensor: + """Dequantize from uint8 back to float.""" + # Infer group size: scales has shape (n_groups, 1), so n_groups = scales.shape[0] + total = w.numel() + n_groups = scales.shape[0] if scales.dim() > 1 else scales.numel() + group_size = total // n_groups if n_groups > 0 else total + + float_w = w.to(scales.dtype).reshape(-1, group_size) + float_w = (float_w - zeros) * scales + return float_w.reshape(original_shape) + + +@torch.no_grad() +def _quantize_stochastic( + w: torch.Tensor, + q_group_size: int = -1, + n_bit: int = 8, +) -> tuple: + """Asymmetric min-max quantization with stochastic rounding. + + Instead of deterministic ``round()``, the rounding direction is chosen + probabilistically proportional to the fractional part. This gives an + unbiased estimator of the original value in expectation. + + Returns: + ``(quantized_uint8, scales, zeros, original_shape)`` + """ + org_shape = w.shape + if q_group_size > 0: + assert w.nelement() % q_group_size == 0 + w = w.reshape(-1, q_group_size) + assert w.dim() == 2 + + max_val = w.amax(dim = 1, keepdim = True) + min_val = w.amin(dim = 1, keepdim = True) + max_int = 2**n_bit - 1 + min_int = 0 + scales = (max_val - min_val).clamp(min = 1e-5) / max_int + zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int) + + w_scaled = w / scales + up = torch.ceil(w_scaled) + down = torch.floor(w_scaled) + prob = w_scaled - down + rng = torch.rand_like(prob) + w = torch.where(rng < prob, up, down) + w = torch.clamp(w + zeros, min_int, max_int) + w = w.reshape(org_shape).to(torch.uint8) + + return w, scales, zeros, org_shape diff --git a/unsloth/tokenizer_utils.py b/unsloth/tokenizer_utils.py index 96c22f62ff..8be6bb5a5a 100644 --- a/unsloth/tokenizer_utils.py +++ b/unsloth/tokenizer_utils.py @@ -42,6 +42,7 @@ __all__ = [ "check_tokenizer", "add_new_tokens", "fix_sentencepiece_gguf", + "get_tokenizer_info", ] @@ -896,6 +897,55 @@ def check_tokenizer( return convert_to_fast_tokenizer(tokenizer) +def get_tokenizer_info(tokenizer) -> dict: + """Return a concise diagnostic summary of a tokenizer instance. + + Collects key properties into a plain dict suitable for logging, debugging, + or displaying in the Unsloth Studio UI. All fields are safe to access — + missing attributes fall back to ``None`` rather than raising. + + Example output:: + + { + "name_or_path": "unsloth/Llama-3.2-1B-Instruct", + "tokenizer_class": "PreTrainedTokenizerFast", + "is_fast": True, + "vocab_size": 128000, + "added_tokens_count": 256, + "model_max_length": 131072, + "padding_side": "right", + "bos_token": "<|begin_of_text|>", + "eos_token": "<|eot_id|>", + "pad_token": "<|finetune_right_pad_id|>", + "unk_token": None, + "has_chat_template": True, + "special_tokens_count": 3, + } + + Args: + tokenizer: Any HuggingFace ``PreTrainedTokenizer`` or + ``PreTrainedTokenizerFast`` instance. + + Returns: + A ``dict`` of tokenizer properties. Safe to serialize to JSON. + """ + return { + "name_or_path": getattr(tokenizer, "name_or_path", None), + "tokenizer_class": type(tokenizer).__name__, + "is_fast": getattr(tokenizer, "is_fast", False), + "vocab_size": getattr(tokenizer, "vocab_size", None), + "added_tokens_count": len(getattr(tokenizer, "added_tokens_decoder", {})), + "model_max_length": getattr(tokenizer, "model_max_length", None), + "padding_side": getattr(tokenizer, "padding_side", None), + "bos_token": getattr(tokenizer, "bos_token", None), + "eos_token": getattr(tokenizer, "eos_token", None), + "pad_token": getattr(tokenizer, "pad_token", None), + "unk_token": getattr(tokenizer, "unk_token", None), + "has_chat_template": getattr(tokenizer, "chat_template", None) is not None, + "special_tokens_count": len(getattr(tokenizer, "all_special_tokens", [])), + } + + import inspect from inspect import getsource import trl diff --git a/unsloth/trainer.py b/unsloth/trainer.py index 8bb4440021..eea985e958 100644 --- a/unsloth/trainer.py +++ b/unsloth/trainer.py @@ -17,7 +17,7 @@ import os import psutil import warnings from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, List from functools import wraps import trl @@ -46,6 +46,7 @@ __all__ = [ "unsloth_train", "_patch_trl_trainer", "UnslothVisionDataCollator", + "QGaloreConfig", ] logger = logging.getLogger(__name__) @@ -130,8 +131,39 @@ except: from transformers import TrainingArguments +@dataclass +class QGaloreConfig: + """Configuration for Q-GaLore optimizer integration. + + Pass an instance of this class to ``UnslothTrainingArguments`` (via + ``q_galore_config``) to enable Q-GaLore training. + """ + + rank: int = 256 + update_proj_gap: int = 200 + scale: float = 0.25 + proj_quant: bool = True + proj_quant_group_size: int = -1 + proj_quant_n_bit: int = 4 + weight_quant: bool = False + stochastic_round: bool = True + weight_group_size: int = 128 + cos_threshold: float = 0.4 + gamma_proj: float = 2.0 + queue_size: int = 5 + target_modules: Optional[List[str]] = None + + class UnslothTrainingArguments(TrainingArguments): - def __init__(self, embedding_learning_rate: float = None, *args, **kwargs): + def __init__( + self, + embedding_learning_rate: float = None, + q_galore_config: Optional[QGaloreConfig] = None, + *args, + **kwargs, + ): + self.q_galore_config = q_galore_config + self.embedding_learning_rate = embedding_learning_rate super().__init__(*args, **kwargs) self.embedding_learning_rate = embedding_learning_rate @@ -181,6 +213,13 @@ def _create_unsloth_optimizer( class UnslothTrainer(SFTTrainer): def create_optimizer(self): + # --- Q-GaLore optimizer --- + q_galore_config = getattr(self.args, "q_galore_config", None) + if q_galore_config is not None and self.optimizer is None: + embedding_lr = getattr(self.args, "embedding_learning_rate", None) + return self._create_q_galore_optimizer(q_galore_config, embedding_lr) + + # --- Embedding-LR optimizer --- embedding_learning_rate = getattr(self.args, "embedding_learning_rate", None) if embedding_learning_rate is None: return super().create_optimizer() @@ -197,6 +236,105 @@ class UnslothTrainer(SFTTrainer): ) return self.optimizer + def _create_q_galore_optimizer(self, config: "QGaloreConfig", embedding_lr = None): + """Build the Q-GaLore optimizer from a QGaloreConfig.""" + from unsloth.optimizers.q_galore_adamw import ( + QGaLoreAdamW8bit, + make_q_galore_param_groups, + install_weight_quant_hooks, + ) + + lr = self.args.learning_rate + weight_decay = self.args.weight_decay + + param_groups = make_q_galore_param_groups( + self.model, + lr = lr, + weight_decay = weight_decay, + rank = config.rank, + update_proj_gap = config.update_proj_gap, + scale = config.scale, + proj_quant = config.proj_quant, + proj_quant_group_size = config.proj_quant_group_size, + proj_quant_n_bit = config.proj_quant_n_bit, + weight_quant = config.weight_quant, + stochastic_round = config.stochastic_round, + weight_group_size = config.weight_group_size, + cos_threshold = config.cos_threshold, + gamma_proj = config.gamma_proj, + queue_size = config.queue_size, + target_modules = config.target_modules, + ) + + # --- Split embedding params with custom LR (Fix #2) --- + if embedding_lr is not None: + # Build a fast param->name lookup (O(N) instead of O(N*M)) + param_to_name = {id(p): name for name, p in self.model.named_parameters()} + + new_groups = [] + for group in param_groups: + if "rank" in group: + # GaLore group — keep as-is (embeddings are never in here) + new_groups.append(group) + continue + # Non-GaLore group: split out embedding params + embed_params = [] + other_params = [] + for p in group["params"]: + # Check if this param belongs to a modules_to_save embedding + name = param_to_name.get(id(p)) + if name and name.endswith("modules_to_save.default.weight"): + partial_name = name[: -len(".modules_to_save.default.weight")] + partial_name = partial_name[partial_name.rfind(".") + 1 :] + print( + f"Unsloth: Setting lr = {embedding_lr:.2e} instead of {lr:.2e} for {partial_name}." + ) + embed_params.append(p) + else: + other_params.append(p) + if other_params: + other_group = dict(group) + other_group["params"] = other_params + new_groups.append(other_group) + if embed_params: + embed_group = dict(group) + embed_group["params"] = embed_params + embed_group["lr"] = embedding_lr + new_groups.append(embed_group) + param_groups = new_groups + + # --- Forward optimizer hyperparameters (Fix #3) --- + self.optimizer = QGaLoreAdamW8bit( + param_groups, + lr = lr, + weight_decay = weight_decay, + betas = (self.args.adam_beta1, self.args.adam_beta2), + eps = self.args.adam_epsilon, + ) + + # Initialize INT8 weight quantization if enabled + if config.weight_quant: + QGaLoreAdamW8bit.init_weight_quantization( + self.model, + param_groups, + group_size = config.weight_group_size, + stochastic = config.stochastic_round, + ) + # Forward pre-hooks dequantize INT8 weights to float before each + # forward pass, allowing the optimizer to free float weight memory + # between steps. + install_weight_quant_hooks(self.model) + + n_galore = sum(len(g["params"]) for g in param_groups if "rank" in g) + n_other = sum(len(g["params"]) for g in param_groups if "rank" not in g) + print( + f"🦥 Unsloth: Q-GaLore enabled — " + f"{n_galore} GaLore params (rank={config.rank}), " + f"{n_other} standard params." + ) + + return self.optimizer + # From `trl>=0.13.0`, they changed how to pass several params to the trainer # We need to patch to make the transition smooth diff --git a/unsloth_cli/__init__.py b/unsloth_cli/__init__.py index 3b9043c5bf..3a821359b7 100644 --- a/unsloth_cli/__init__.py +++ b/unsloth_cli/__init__.py @@ -6,7 +6,6 @@ import typer from unsloth_cli.commands.train import train from unsloth_cli.commands.inference import inference from unsloth_cli.commands.export import export, list_checkpoints -from unsloth_cli.commands.ui import ui from unsloth_cli.commands.studio import studio_app app = typer.Typer( @@ -18,5 +17,4 @@ app.command()(train) app.command()(inference) app.command()(export) app.command("list-checkpoints")(list_checkpoints) -app.command()(ui) app.add_typer(studio_app, name = "studio", help = "Unsloth Studio commands.") diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index 192e138a9c..c6d398eebd 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -22,9 +22,9 @@ _PACKAGE_ROOT = Path(__file__).resolve().parent.parent.parent def _studio_venv_python() -> Optional[Path]: """Return the studio venv Python binary, or None if not set up.""" if platform.system() == "Windows": - p = STUDIO_HOME / ".venv" / "Scripts" / "python.exe" + p = STUDIO_HOME / "unsloth_studio" / "Scripts" / "python.exe" else: - p = STUDIO_HOME / ".venv" / "bin" / "python" + p = STUDIO_HOME / "unsloth_studio" / "bin" / "python" return p if p.is_file() else None @@ -44,7 +44,7 @@ def _find_run_py() -> Optional[Path]: "lib/python*/site-packages/studio/backend/run.py", "Lib/site-packages/studio/backend/run.py", ): - for match in (STUDIO_HOME / ".venv").glob(pattern): + for match in (STUDIO_HOME / "unsloth_studio").glob(pattern): return match return None @@ -64,7 +64,7 @@ def _find_setup_script() -> Optional[Path]: f"lib/python*/site-packages/studio/{name}", f"Lib/site-packages/studio/{name}", ): - for match in (STUDIO_HOME / ".venv").glob(pattern): + for match in (STUDIO_HOME / "unsloth_studio").glob(pattern): return match return None @@ -85,7 +85,7 @@ def studio_default( return # Always use the studio venv if it exists and we're not already in it - studio_venv_dir = STUDIO_HOME / ".venv" + studio_venv_dir = STUDIO_HOME / "unsloth_studio" in_studio_venv = sys.prefix.startswith(str(studio_venv_dir)) if not in_studio_venv: @@ -132,7 +132,7 @@ def studio_default( else: os.execvp(str(studio_python), args) else: - typer.echo("Studio not set up. Run 'unsloth studio setup' first.") + typer.echo("Studio not set up. Run install.sh first.") raise typer.Exit(1) from studio.backend.run import run_server @@ -166,12 +166,11 @@ def studio_default( typer.echo("\nShutting down...") -# ── unsloth studio setup ───────────────────────────────────────────── +# ── unsloth studio setup / update ───────────────────────────────────── -@studio_app.command() -def setup(): - """Run one-time Studio environment setup.""" +def _run_setup_script() -> None: + """Find and run the studio setup/update script.""" script = _find_setup_script() if not script: typer.echo("Error: Could not find setup script (setup.sh / setup.ps1).") @@ -188,6 +187,35 @@ def setup(): raise typer.Exit(result.returncode) +@studio_app.command(hidden = True) +def setup(): + """Deprecated: use 'unsloth studio update' or re-run install.sh.""" + typer.echo( + "Note: 'unsloth studio setup' is deprecated. Use 'unsloth studio update' or re-run install.sh." + ) + _run_setup_script() + + +@studio_app.command() +def update( + local: bool = typer.Option( + False, "--local", help = "Install from local repo instead of PyPI" + ), + package: str = typer.Option( + "unsloth", "--package", help = "Package name to install/update (for testing)" + ), +): + """Update Unsloth Studio dependencies and rebuild.""" + os.environ["STUDIO_LOCAL_INSTALL"] = "1" if local else "0" + os.environ["STUDIO_PACKAGE_NAME"] = package + if local: + # Pass the repo root explicitly so install_python_stack.py doesn't + # have to guess from SCRIPT_DIR (which may be inside site-packages). + repo_root = Path(__file__).resolve().parents[2] + os.environ["STUDIO_LOCAL_REPO"] = str(repo_root) + _run_setup_script() + + # ── unsloth studio reset-password ──────────────────────────────────── diff --git a/unsloth_cli/commands/ui.py b/unsloth_cli/commands/ui.py deleted file mode 100644 index 8f76636990..0000000000 --- a/unsloth_cli/commands/ui.py +++ /dev/null @@ -1,103 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -import os -import sys -import time -from pathlib import Path -from typing import Optional - -import typer - - -def ui( - port: int = typer.Option( - 8888, "--port", "-p", help = "Port to run the UI server on." - ), - host: str = typer.Option( - "0.0.0.0", "--host", "-H", help = "Host address to bind to." - ), - frontend: Optional[Path] = typer.Option( - None, "--frontend", "-f", help = "Path to frontend build directory." - ), - silent: bool = typer.Option( - False, "--silent", "-q", help = "Suppress startup messages." - ), -): - """Launch the Unsloth web UI backend server (alias for 'unsloth studio').""" - from unsloth_cli.commands.studio import ( - _studio_venv_python, - _find_run_py, - STUDIO_HOME, - ) - - # Re-execute in studio venv if available and not already inside it - studio_venv_dir = STUDIO_HOME / ".venv" - in_studio_venv = sys.prefix.startswith(str(studio_venv_dir)) - - if not in_studio_venv: - studio_python = _studio_venv_python() - run_py = _find_run_py() - if studio_python and run_py: - if not silent: - typer.echo("Launching Unsloth Studio... Please wait...") - args = [ - str(studio_python), - str(run_py), - "--host", - host, - "--port", - str(port), - ] - if frontend: - args.extend(["--frontend", str(frontend)]) - if silent: - args.append("--silent") - # On Windows, os.execvp() spawns a child but the parent lingers, - # so Ctrl+C only kills the parent leaving the child orphaned. - # Use subprocess.run() on Windows so the parent waits for the child. - if sys.platform == "win32": - import subprocess as _sp - - proc = _sp.Popen(args) - try: - rc = proc.wait() - except KeyboardInterrupt: - # Child has its own signal handler — let it finish - rc = proc.wait() - raise typer.Exit(rc) - else: - os.execvp(str(studio_python), args) - else: - typer.echo("Studio not set up. Run 'unsloth studio setup' first.") - raise typer.Exit(1) - - from studio.backend.run import run_server - - if not silent: - from studio.backend.run import _resolve_external_ip - - display_host = _resolve_external_ip() if host == "0.0.0.0" else host - typer.echo(f"Starting Unsloth Studio on http://{display_host}:{port}") - - run_kwargs = dict(host = host, port = port, silent = silent) - if frontend is not None: - run_kwargs["frontend_path"] = frontend - run_server(**run_kwargs) - - from studio.backend.run import _shutdown_event - - try: - if _shutdown_event is not None: - # NOTE: Event.wait() without a timeout blocks at the C level - # on Linux, preventing Python from delivering SIGINT (Ctrl+C). - while not _shutdown_event.is_set(): - _shutdown_event.wait(timeout = 1) - else: - while True: - time.sleep(1) - except KeyboardInterrupt: - from studio.backend.run import _graceful_shutdown, _server - - _graceful_shutdown(_server) - typer.echo("\nShutting down...")