diff --git a/studio/backend/run.py b/studio/backend/run.py index b89203756..87fcdc01d 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -24,6 +24,7 @@ if str(backend_dir) not in sys.path: import _platform_compat # noqa: F401 from loggers import get_logger +from startup_banner import print_studio_access_banner logger = get_logger(__name__) @@ -338,19 +339,11 @@ def run_server( if not silent: display_host = _resolve_external_ip() if host == "0.0.0.0" else host - - print("") - print("=" * 50) - print(f"🦥 Open your web browser, and enter http://localhost:{port}") - print("=" * 50) - print("") - print("=" * 50) - print(f"🦥 Unsloth Studio is running on port {port}") - print(f" Local Access: http://localhost:{port}") - print(f" Worldwide Web Address: http://{display_host}:{port}") - print(f" API: http://{display_host}:{port}/api") - print(f" Health: http://{display_host}:{port}/api/health") - print("=" * 50) + print_studio_access_banner( + port = port, + bind_host = host, + display_host = display_host, + ) return app diff --git a/studio/backend/startup_banner.py b/studio/backend/startup_banner.py new file mode 100644 index 000000000..54acac054 --- /dev/null +++ b/studio/backend/startup_banner.py @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Terminal banner for Studio startup. + +Stdlib only — safe to import without the rest of the backend (no structlog/uvicorn). +""" + +from __future__ import annotations + +import os +import sys + + +def stdout_supports_color() -> bool: + """True if we should emit ANSI colors.""" + if os.environ.get("NO_COLOR", "").strip(): + return False + if os.environ.get("FORCE_COLOR", "").strip(): + return True + try: + return sys.stdout.isatty() + except Exception: + return False + + +def print_port_in_use_notice(original_port: int, new_port: int) -> None: + """Message when the requested port is taken and another is chosen.""" + msg = f"Port {original_port} is in use, using port {new_port} instead." + if stdout_supports_color(): + print(f"\033[38;5;245m{msg}\033[0m") + else: + print(msg) + + +def print_studio_access_banner( + *, + port: int, + bind_host: str, + display_host: str, +) -> None: + """Pretty-print URLs after the server is listening (beginner-friendly).""" + use_color = stdout_supports_color() + dim = "\033[38;5;245m" + title = "\033[38;5;150m" + local_url_style = "\033[38;5;108;1m" + secondary = "\033[38;5;109m" + reset = "\033[0m" + + def style(text: str, code: str) -> str: + return f"{code}{text}{reset}" if use_color else text + + ipv6_bind = bind_host in ("::", "::1") + if ipv6_bind: + local_url = f"http://[::1]:{port}" + alt_local = f"http://localhost:{port}" + else: + local_url = f"http://127.0.0.1:{port}" + alt_local = f"http://localhost:{port}" + if ":" in display_host: + external_url = f"http://[{display_host}]:{port}" + else: + external_url = f"http://{display_host}:{port}" + listen_all = bind_host in ("0.0.0.0", "::") + loopback_bind = bind_host in ("127.0.0.1", "localhost", "::1") + api_base = local_url if listen_all or loopback_bind else external_url + + lines: list[str] = [ + "", + style("🦥 Unsloth Studio is running", title), + style("─" * 52, dim), + style(" On this machine — open this in your browser:", dim), + style(f" {local_url}", local_url_style), + style(f" (same as {alt_local})", dim), + ] + + if listen_all and display_host not in ( + "127.0.0.1", + "localhost", + "::1", + "0.0.0.0", + "::", + ): + lines.extend( + [ + "", + style(" From another device on your network / to share:", dim), + style(f" {external_url}", secondary), + ] + ) + elif not listen_all and bind_host not in ("127.0.0.1", "localhost", "::1"): + lines.extend( + [ + "", + style(" Bound address:", dim), + style(f" {external_url}", secondary), + ] + ) + + lines.extend( + [ + "", + style(" API & health:", dim), + style(f" {api_base}/api", secondary), + style(f" {api_base}/api/health", secondary), + style("─" * 52, dim), + style( + " Tip: if you are on the same computer, use the Local link above.", + dim, + ), + "", + ] + ) + + print("\n".join(lines)) diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index bcd17c4d8..f6fd38d5d 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -45,6 +45,7 @@ NO_TORCH = _infer_no_torch() # -- 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: +# CLI: unsloth studio setup --verbose # 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" @@ -96,15 +97,18 @@ def _safe_print(*args: object, **kwargs: object) -> None: ) -# -- Color support ------------------------------------------------------ +# ── Color support ────────────────────────────────────────────────────── +# Same logic as startup_banner: NO_COLOR disables, FORCE_COLOR or TTY enables. -def _enable_colors() -> bool: - """Try to enable ANSI color support. Returns True if available.""" - if not hasattr(sys.stdout, "fileno"): +def _stdout_supports_color() -> bool: + """True if we should emit ANSI colors (matches startup_banner).""" + if os.environ.get("NO_COLOR", "").strip(): return False + if os.environ.get("FORCE_COLOR", "").strip(): + return True try: - if not os.isatty(sys.stdout.fileno()): + if not sys.stdout.isatty(): return False except Exception: return False @@ -113,24 +117,26 @@ def _enable_colors() -> bool: import ctypes kernel32 = ctypes.windll.kernel32 - # Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004) on stdout - handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE + handle = kernel32.GetStdHandle(-11) mode = ctypes.c_ulong() kernel32.GetConsoleMode(handle, ctypes.byref(mode)) kernel32.SetConsoleMode(handle, mode.value | 0x0004) - return True except Exception: return False - return True # Unix terminals support ANSI by default + return True -# 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 +_HAS_COLOR = _stdout_supports_color() + + +# Column layout — matches setup.sh step() helper: +# 2-space indent, 15-char label (dim), then value. +_LABEL = "deps" +_COL = 15 def _green(msg: str) -> str: - return f"\033[92m{msg}\033[0m" if _HAS_COLOR else msg + return f"\033[38;5;108m{msg}\033[0m" if _HAS_COLOR else msg def _cyan(msg: str) -> str: @@ -141,21 +147,39 @@ def _red(msg: str) -> str: return f"\033[91m{msg}\033[0m" if _HAS_COLOR else msg -def _progress(label: str) -> None: - """Print an in-place progress bar for the current install step. +def _dim(msg: str) -> str: + return f"\033[38;5;245m{msg}\033[0m" if _HAS_COLOR else msg - 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. - """ + +def _title(msg: str) -> str: + return f"\033[38;5;150m{msg}\033[0m" if _HAS_COLOR else msg + + +_RULE = "\u2500" * 52 + + +def _step(label: str, value: str, color_fn = None) -> None: + """Print a single step line in the column format.""" + if color_fn is None: + color_fn = _green + padded = label[:_COL] + print(f" {_dim(padded)}{' ' * (_COL - len(padded))}{color_fn(value)}") + + +def _progress(label: str) -> None: + """Print an in-place progress bar aligned to the step column layout.""" global _STEP _STEP += 1 if VERBOSE: - return # verbose mode: run() already printed the label + return width = 20 filled = int(width * _STEP / _TOTAL) bar = "=" * filled + "-" * (width - filled) - end = "\n" if _STEP >= _TOTAL else "" # newline only on the final step - sys.stdout.write(f"\r[{bar}] {_STEP:2}/{_TOTAL} {label:<40}{end}") + pad = " " * (_COL - len(_LABEL)) + end = "\n" if _STEP >= _TOTAL else "" + sys.stdout.write( + f"\r {_dim(_LABEL)}{pad}[{bar}] {_STEP:2}/{_TOTAL} {label:<20}{end}" + ) sys.stdout.flush() @@ -164,14 +188,14 @@ def run( ) -> subprocess.CompletedProcess[bytes]: """Run a command; on failure print output and exit.""" if VERBOSE: - print(f" {label}...") + _step(_LABEL, f"{label}...", _dim) result = subprocess.run( cmd, stdout = subprocess.PIPE if quiet else None, stderr = subprocess.STDOUT if quiet else None, ) if result.returncode != 0: - _safe_print(_red(f"❌ {label} failed (exit code {result.returncode}):")) + _step("error", f"{label} failed (exit code {result.returncode})", _red) if result.stdout: print(result.stdout.decode(errors = "replace")) sys.exit(result.returncode) @@ -353,9 +377,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: text = True, ) if result.returncode != 0: - _safe_print( - _red(f" ⚠️ Could not find package {package_name}, skipping patch") - ) + _step(_LABEL, f"package {package_name} not found, skipping patch", _red) return location = None @@ -365,11 +387,11 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: break if not location: - _safe_print(_red(f" ⚠️ Could not determine location of {package_name}")) + _step(_LABEL, f"could not locate {package_name}", _red) return dest = Path(location) / relative_path - print(_cyan(f" Patching {dest.name} in {package_name}...")) + _step(_LABEL, f"patching {dest.name} in {package_name}...", _dim) download_file(url, dest) @@ -633,7 +655,7 @@ def install_python_stack() -> int: stderr = subprocess.DEVNULL, ) - _safe_print(_green("✅ Python dependencies installed")) + _step(_LABEL, "installed") return 0 diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 42bae4281..8a1ae4b23 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -10,13 +10,28 @@ full setup including frontend build. Supports NVIDIA GPU (full training + inference) and CPU-only (GGUF chat mode). .NOTES - Usage: powershell -ExecutionPolicy Bypass -File setup.ps1 + Default output is minimal (step/substep), aligned with studio/setup.sh. + + FULL / LEGACY LOGGING (defensible audit trail, multi-line [OK]/[WARN]/paths): + unsloth studio setup --verbose + (sets UNSLOTH_VERBOSE=1; same as install_python_stack.py) + Or: $env:UNSLOTH_VERBOSE='1'; powershell -File .\studio\setup.ps1 + Or: .\setup.ps1 --verbose #> $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $PackageDir = Split-Path -Parent $ScriptDir +# Same as: unsloth studio setup --verbose (see unsloth_cli/commands/studio.py) +foreach ($a in $args) { + if ($a -eq '--verbose' -or $a -eq '-v') { + $env:UNSLOTH_VERBOSE = '1' + break + } +} +$script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq '1') + # Detect if running from pip install (no frontend/ dir in studio) $FrontendDir = Join-Path $ScriptDir "frontend" $OxcValidatorDir = Join-Path $ScriptDir "backend\core\data_recipe\oxc-validator" @@ -247,17 +262,138 @@ function Find-VsBuildTools { return $null } +# ───────────────────────────────────────────── +# Output style (aligned with studio/setup.sh: step / substep) +# ───────────────────────────────────────────── +$Rule = [string]::new([char]0x2500, 52) + +function Enable-StudioVirtualTerminal { + if ($env:NO_COLOR) { return $false } + try { + Add-Type -Namespace StudioVT -Name Native -MemberDefinition @' +[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int nStdHandle); +[DllImport("kernel32.dll")] public static extern bool GetConsoleMode(IntPtr h, out uint m); +[DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr h, uint m); +'@ -ErrorAction Stop + $h = [StudioVT.Native]::GetStdHandle(-11) + [uint32]$mode = 0 + if (-not [StudioVT.Native]::GetConsoleMode($h, [ref]$mode)) { return $false } + $mode = $mode -bor 0x0004 + return [StudioVT.Native]::SetConsoleMode($h, $mode) + } catch { + return $false + } +} +$script:StudioVtOk = Enable-StudioVirtualTerminal + +function Get-StudioAnsi { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Title', 'Dim', 'Ok', 'Warn', 'Err', 'Reset')] + [string]$Kind + ) + $e = [char]27 + switch ($Kind) { + 'Title' { return "${e}[38;5;150m" } + 'Dim' { return "${e}[38;5;245m" } + 'Ok' { return "${e}[38;5;108m" } + 'Warn' { return "${e}[38;5;136m" } + 'Err' { return "${e}[91m" } + 'Reset' { return "${e}[0m" } + } +} + +function Write-SetupVerboseDetail { + param( + [Parameter(Mandatory = $true)][string]$Message, + [string]$Color = "Gray" + ) + if (-not $script:UnslothVerbose) { return } + if ($script:StudioVtOk -and -not $env:NO_COLOR) { + $ansi = switch ($Color) { + 'Green' { (Get-StudioAnsi Ok) } + 'Gray' { (Get-StudioAnsi Dim) } + 'DarkGray' { (Get-StudioAnsi Dim) } + 'Yellow' { (Get-StudioAnsi Warn) } + 'Cyan' { (Get-StudioAnsi Title) } + 'Red' { (Get-StudioAnsi Err) } + default { (Get-StudioAnsi Dim) } + } + Write-Host ($ansi + $Message + (Get-StudioAnsi Reset)) + } else { + $fc = switch ($Color) { + 'Green' { 'DarkGreen' } + 'Gray' { 'DarkGray' } + 'Cyan' { 'Green' } + default { $Color } + } + Write-Host $Message -ForegroundColor $fc + } +} + +function step { + param( + [Parameter(Mandatory = $true)][string]$Label, + [Parameter(Mandatory = $true)][string]$Value, + [string]$Color = "Green" + ) + if ($script:StudioVtOk -and -not $env:NO_COLOR) { + $dim = Get-StudioAnsi Dim + $rst = Get-StudioAnsi Reset + $val = switch ($Color) { + 'Green' { Get-StudioAnsi Ok } + 'Yellow' { Get-StudioAnsi Warn } + 'Red' { Get-StudioAnsi Err } + 'DarkGray' { Get-StudioAnsi Dim } + default { Get-StudioAnsi Ok } + } + $padded = if ($Label.Length -ge 15) { $Label.Substring(0, 15) } else { $Label.PadRight(15) } + Write-Host (" {0}{1}{2}{3}{4}{2}" -f $dim, $padded, $rst, $val, $Value) + } else { + $padded = if ($Label.Length -ge 15) { $Label.Substring(0, 15) } else { $Label.PadRight(15) } + Write-Host (" {0}" -f $padded) -NoNewline -ForegroundColor DarkGray + $fc = switch ($Color) { + 'Green' { 'DarkGreen' } + 'Yellow' { 'Yellow' } + 'Red' { 'Red' } + 'DarkGray' { 'DarkGray' } + default { 'DarkGreen' } + } + Write-Host $Value -ForegroundColor $fc + } +} + +function substep { + param( + [Parameter(Mandatory = $true)][string]$Message, + [string]$Color = "DarkGray" + ) + if ($script:StudioVtOk -and -not $env:NO_COLOR) { + $msgCol = switch ($Color) { + 'Yellow' { (Get-StudioAnsi Warn) } + default { (Get-StudioAnsi Dim) } + } + $pad = "".PadRight(15) + Write-Host (" {0}{1}{2}{3}" -f $msgCol, $pad, $Message, (Get-StudioAnsi Reset)) + } else { + $fc = switch ($Color) { + 'Yellow' { 'Yellow' } + default { 'DarkGray' } + } + Write-Host (" {0,-15}{1}" -f "", $Message) -ForegroundColor $fc + } +} + # ───────────────────────────────────────────── # Banner # ───────────────────────────────────────────── -if ($env:SKIP_STUDIO_BASE -eq "1") { - Write-Host "+==============================================+" -ForegroundColor Green - Write-Host "| Unsloth Studio Setup (Windows) |" -ForegroundColor Green - Write-Host "+==============================================+" -ForegroundColor Green +Write-Host "" +if ($script:StudioVtOk -and -not $env:NO_COLOR) { + Write-Host (" " + (Get-StudioAnsi Title) + [char]::ConvertFromUtf32(0x1F9A5) + " Unsloth Studio Setup" + (Get-StudioAnsi Reset)) + Write-Host (" {0}{1}{2}" -f (Get-StudioAnsi Dim), $Rule, (Get-StudioAnsi Reset)) } else { - Write-Host "+==============================================+" -ForegroundColor Green - Write-Host "| Unsloth Studio Update (Windows) |" -ForegroundColor Green - Write-Host "+==============================================+" -ForegroundColor Green + Write-Host (" " + [char]::ConvertFromUtf32(0x1F9A5) + " Unsloth Studio Setup") -ForegroundColor Green + Write-Host " $Rule" -ForegroundColor DarkGray } # ========================================================================== @@ -303,12 +439,12 @@ if (-not $HasNvidiaSmi) { } if (-not $HasNvidiaSmi) { Write-Host "" - Write-Host "[WARN] No NVIDIA GPU detected. Studio will run in chat-only (GGUF) mode." -ForegroundColor Yellow + step "gpu" "none (chat-only / GGUF)" "Yellow" Write-Host " Training and GPU inference require an NVIDIA GPU with drivers installed." -ForegroundColor Yellow Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow Write-Host "" } else { - Write-Host "[OK] NVIDIA GPU detected" -ForegroundColor Green + step "gpu" "NVIDIA GPU detected" } # ============================================ @@ -364,9 +500,9 @@ if (-not $HasGit) { Write-Host " Install Git from https://git-scm.com/download/win and re-run." -ForegroundColor Red exit 1 } - Write-Host "[OK] Git installed: $(git --version)" -ForegroundColor Green + step "git" "$(git --version)" } else { - Write-Host "[OK] Git found: $(git --version)" -ForegroundColor Green + step "git" "$(git --version)" } # ============================================ @@ -408,14 +544,14 @@ if (-not $HasCmake) { } } if ($HasCmake) { - Write-Host "[OK] CMake installed" -ForegroundColor Green + step "cmake" "installed" } else { Write-Host "[ERROR] CMake is required but could not be installed." -ForegroundColor Red Write-Host " Install CMake from https://cmake.org/download/ and re-run." -ForegroundColor Red exit 1 } } else { - Write-Host "[OK] CMake found: $(cmake --version | Select-Object -First 1)" -ForegroundColor Green + step "cmake" "$(cmake --version | Select-Object -First 1)" } # ============================================ @@ -442,7 +578,7 @@ if (-not $vsResult) { if ($vsResult) { $CmakeGenerator = $vsResult.Generator $VsInstallPath = $vsResult.InstallPath - Write-Host "[OK] $CmakeGenerator detected via $($vsResult.Source)" -ForegroundColor Green + step "vs" "$CmakeGenerator ($($vsResult.Source))" if ($vsResult.ClExe) { Write-Host " cl.exe: $($vsResult.ClExe)" -ForegroundColor Gray } } else { Write-Host "[ERROR] Visual Studio Build Tools could not be found or installed." -ForegroundColor Red @@ -713,7 +849,7 @@ if ($VsInstallPath -and $CudaToolkitRoot) { } } -Write-Host "[OK] CUDA Toolkit: $NvccPath" -ForegroundColor Green +step "cuda" $NvccPath Write-Host " CUDA_PATH = $CudaToolkitRoot" -ForegroundColor Gray Write-Host " CudaToolkitDir = $CudaToolkitRoot\" -ForegroundColor Gray @@ -730,7 +866,7 @@ if (-not $CudaArch) { # 1f. Node.js / npm (skip if pip-installed -- only needed for frontend build) # ============================================ if ($IsPipInstall) { - Write-Host "[OK] Running from pip install - frontend already bundled, skipping Node/npm check" -ForegroundColor Green + step "frontend" "bundled (pip install)" } else { # setup.sh installs Node LTS (v22) via nvm. We enforce the same range here: # Vite 8 requires Node ^20.19.0 || >=22.12.0, npm >= 11. @@ -771,7 +907,7 @@ if ($IsPipInstall) { } } - Write-Host "[OK] Node $(node -v) | npm $(npm -v)" -ForegroundColor Green + step "node" "$(node -v) | npm $(npm -v)" # ── bun (optional, faster package installs) ── # Installed via npm — Node is already guaranteed above. Works on all platforms. @@ -825,7 +961,7 @@ if ($HasPython) { Write-Host " Install Python 3.12 from https://python.org/downloads/" -ForegroundColor Yellow exit 1 } - Write-Host "[OK] Python $(python --version)" -ForegroundColor Green + step "python" "$(python --version 2>&1)" $PythonOk = $true } @@ -860,7 +996,7 @@ $DistDir = Join-Path $FrontendDir "dist" $NeedFrontendBuild = $true if ($IsPipInstall) { $NeedFrontendBuild = $false - Write-Host "[OK] Running from pip install - frontend already bundled, skipping build" -ForegroundColor Green + step "frontend" "bundled (pip install)" } elseif (Test-Path $DistDir) { $DistTime = (Get-Item $DistDir).LastWriteTime $NewerFile = $null @@ -881,7 +1017,7 @@ if ($IsPipInstall) { } if (-not $NewerFile) { $NeedFrontendBuild = $false - Write-Host "[OK] Frontend already built and up to date -- skipping build" -ForegroundColor Green + step "frontend" "up to date" } else { Write-Host "[INFO] Frontend source changed since last build -- rebuilding..." -ForegroundColor Yellow } @@ -992,10 +1128,9 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { $CssFiles = Get-ChildItem (Join-Path $DistDir "assets") -Filter "*.css" -ErrorAction SilentlyContinue $MaxCssSize = ($CssFiles | Measure-Object -Property Length -Maximum).Maximum if ($MaxCssSize -lt 100000) { - Write-Host "[WARN] Largest CSS file is only $([math]::Round($MaxCssSize / 1024))KB -- Tailwind may not have scanned all source files." -ForegroundColor Yellow - Write-Host " Expected >100KB. Check for .gitignore files blocking the Tailwind oxide scanner." -ForegroundColor Yellow + step "frontend" "built (warning: CSS may be truncated)" "Yellow" } else { - Write-Host "[OK] Frontend built to frontend/dist (CSS: $([math]::Round($MaxCssSize / 1024))KB)" -ForegroundColor Green + step "frontend" "built" } } @@ -1319,7 +1454,7 @@ if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] Could not install tiktoken into .venv_t5/ -- Qwen tokenizers may fail" -ForegroundColor Yellow } $ErrorActionPreference = $prevEAP_t5 -Write-Host "[OK] Transformers 5.x pre-installed to .venv_t5/" -ForegroundColor Green +step "transformers" "5.x pre-installed" # ========================================================================== # PHASE 3.4: Prefer prebuilt llama.cpp bundles before source build @@ -1400,7 +1535,7 @@ if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") { $ErrorActionPreference = $prevEAPPrebuilt if ($prebuiltExit -eq 0) { - Write-Host "[OK] Prebuilt llama.cpp installed and validated" -ForegroundColor Green + step "llama.cpp" "prebuilt installed and validated" } else { if (Test-Path $LlamaCppDir) { Write-Host "[WARN] Prebuilt update failed; existing install was restored or cleaned before source build fallback" -ForegroundColor Yellow @@ -1495,10 +1630,10 @@ if (Test-Path $LlamaServerBin) { if (-not $NeedLlamaSourceBuild) { Write-Host "" - Write-Host "[OK] Using validated prebuilt llama.cpp install at $LlamaCppDir" -ForegroundColor Green + step "llama.cpp" "prebuilt (validated)" } elseif ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) { Write-Host "" - Write-Host "[OK] llama-server already exists at $LlamaServerBin" -ForegroundColor Green + step "llama.cpp" "already built" } elseif (-not $HasCmakeForBuild) { Write-Host "" if (-not $HasNvidiaSmi) { @@ -1719,38 +1854,37 @@ if (-not $NeedLlamaSourceBuild) { $totalSec = [math]::Round($totalSw.Elapsed.TotalSeconds % 60, 1) # -- Summary -- - Write-Host "" if ($BuildOk -and (Test-Path $LlamaServerBin)) { - Write-Host "[OK] llama-server built at $LlamaServerBin" -ForegroundColor Green + step "llama.cpp" "built" $QuantizeBin = Join-Path $BuildDir "bin\Release\llama-quantize.exe" if (Test-Path $QuantizeBin) { - Write-Host "[OK] llama-quantize available for GGUF export" -ForegroundColor Green + step "llama-quantize" "built" } - Write-Host " Build time: ${totalMin}m ${totalSec}s" -ForegroundColor Cyan + step "build time" "${totalMin}m ${totalSec}s" "DarkGray" } else { - # Check alternate paths (some cmake generators don't use Release subdir) $altBin = Join-Path $BuildDir "bin\llama-server.exe" if ($BuildOk -and (Test-Path $altBin)) { - Write-Host "[OK] llama-server built at $altBin" -ForegroundColor Green - Write-Host " Build time: ${totalMin}m ${totalSec}s" -ForegroundColor Cyan + step "llama.cpp" "built" + step "build time" "${totalMin}m ${totalSec}s" "DarkGray" } else { - Write-Host "[FAILED] llama.cpp build failed at step: $FailedStep (${totalMin}m ${totalSec}s)" -ForegroundColor Red - Write-Host " To retry: delete $LlamaCppDir and re-run setup." -ForegroundColor Yellow + step "llama.cpp" "build failed at: $FailedStep (${totalMin}m ${totalSec}s)" "Red" + substep "To retry: delete $LlamaCppDir and re-run setup." "Yellow" exit 1 } } } -# ============================================ -# Done -# ============================================ +# ───────────────────────────────────────────── +# Footer +# ───────────────────────────────────────────── +if ($script:StudioVtOk -and -not $env:NO_COLOR) { + Write-Host (" {0}{1}{2}" -f (Get-StudioAnsi Dim), $Rule, (Get-StudioAnsi Reset)) + Write-Host (" " + (Get-StudioAnsi Title) + "Unsloth Studio Installed" + (Get-StudioAnsi Reset)) + Write-Host (" {0}{1}{2}" -f (Get-StudioAnsi Dim), $Rule, (Get-StudioAnsi Reset)) +} else { + Write-Host " $Rule" -ForegroundColor DarkGray + Write-Host " Unsloth Studio Installed" -ForegroundColor Green + Write-Host " $Rule" -ForegroundColor DarkGray +} +step "launch" "unsloth studio -H 0.0.0.0 -p 8888" Write-Host "" -$doneLine = if ($env:SKIP_STUDIO_BASE -eq "1") { "Setup Complete!" } else { "Update Complete!" } -$doneContent = " $doneLine" -Write-Host "+===============================================+" -ForegroundColor Green -Write-Host ("|" + $doneContent.PadRight(47) + "|") -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 -Write-Host "| |" -ForegroundColor Green -Write-Host "+===============================================+" -ForegroundColor Green diff --git a/studio/setup.sh b/studio/setup.sh index 6f4344675..750227027 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -6,6 +6,27 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +RULE=$(printf '\342\224\200%.0s' {1..52}) + +# ── Colors (same palette as startup_banner / install_python_stack) ── +if [ -n "${NO_COLOR:-}" ]; then + C_TITLE= C_DIM= C_OK= C_WARN= C_ERR= C_RST= +elif [ -t 1 ] || [ -n "${FORCE_COLOR:-}" ]; then + C_TITLE=$'\033[38;5;150m' + C_DIM=$'\033[38;5;245m' + C_OK=$'\033[38;5;108m' + C_WARN=$'\033[38;5;136m' + C_ERR=$'\033[91m' + C_RST=$'\033[0m' +else + C_TITLE= C_DIM= C_OK= C_WARN= C_ERR= C_RST= +fi + +# ── Output helpers ── +# Consistent column layout: 2-space indent, 15-char label (fits llama-quantize), then value. +# Usage: step