From c8be6f6af7e6966852f76acabe1ffbd338b22d5a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 05:21:30 -0700 Subject: [PATCH 1/3] fix(studio): show working reset-password command on Windows login error The backend's "incorrect password" error already returns the correct, PATH-based command ("unsloth studio reset-password"), which the installer puts on PATH on every platform. The auth form rewrote it on Windows into a relative path: .\unsloth_studio\Scripts\unsloth.exe studio reset-password That only resolves when the terminal happens to be inside the Studio home dir (~\.unsloth\studio). From any normal working directory (e.g. C:\Users\) it fails with CommandNotFoundException, so users could not follow the hint to recover a forgotten password. Remove the Windows-only rewrite (and its now-unused usePlatformStore import) and display the backend's command as-is. Frontend typecheck passes (the one remaining tsc error, @tauri-apps/plugin-window-state in provider.tsx, is a pre-existing desktop-only module-resolution issue unrelated to this change). Co-Authored-By: Claude Opus 4.8 --- .../src/features/auth/components/auth-form.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx index d753e1aab5..c1606c7020 100644 --- a/studio/frontend/src/features/auth/components/auth-form.tsx +++ b/studio/frontend/src/features/auth/components/auth-form.tsx @@ -10,7 +10,6 @@ import { Eye, EyeOff } from "lucide-react"; import { useEffect, useState } from "react"; import type { ReactElement } from "react"; import type { SyntheticEvent } from "react"; -import { usePlatformStore } from "@/config/env"; import { refreshSession } from "../api"; // Bootstrap credentials injected into index.html by the backend @@ -294,13 +293,13 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { storeAuthTokens(token.access_token, token.refresh_token); navigate({ to: getPostAuthRoute() }); } catch (err: unknown) { - let msg = err instanceof Error ? err.message : "Auth failed."; - if (msg.includes("unsloth studio reset-password") && usePlatformStore.getState().deviceType === "windows") { - msg = msg.replace( - "unsloth studio reset-password", - ".\\unsloth_studio\\Scripts\\unsloth.exe studio reset-password", - ); - } + // The backend already returns the correct, PATH-based command + // ("unsloth studio reset-password"), which the installer puts on PATH on + // every platform. Do NOT rewrite it to a relative Windows path like + // ".\unsloth_studio\Scripts\unsloth.exe ..." -- that only resolves when the + // terminal happens to be inside the Studio home dir, so it fails with + // CommandNotFoundException everywhere else. Show the backend message as-is. + const msg = err instanceof Error ? err.message : "Auth failed."; setError(msg); } finally { setLoading(false); From 98e0a8161fb93b64f5d24491efe08d70495d56bc Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 05:28:46 -0700 Subject: [PATCH 2/3] fix(studio): make reset-password hint work without PATH (all platforms) Follow-up to the auth-form fix. The hint relied on the `unsloth` launcher being on PATH. That holds in a fresh terminal (the installer adds the launcher dir to PATH), but the command still fails when: - a terminal was opened before install (stale PATH), - ~/.local/bin is not on PATH (the default on macOS), or - a non-interactive / different shell rc was used. Have the backend emit the ABSOLUTE path to this install's own `unsloth` launcher (a sibling of sys.executable) so the command works regardless of PATH or current directory on Windows, macOS, and Linux. POSIX paths are shell-quoted (handles spaces). On Windows the bare absolute path is used only when it has no spaces (works in both cmd and PowerShell); otherwise we fall back to the PATH form to avoid cmd-vs-PowerShell quoting differences. Falls back to `unsloth studio reset-password` if the launcher can't be located. Verified the emitted command runs on Windows (Scripts\unsloth.exe) and Linux (bin/unsloth) -- both resolve `unsloth studio reset-password`. The CLI command name is unchanged, so CI smoke steps that call `unsloth studio reset-password` directly are unaffected. Co-Authored-By: Claude Opus 4.8 --- studio/backend/routes/auth.py | 36 +++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/studio/backend/routes/auth.py b/studio/backend/routes/auth.py index bb4ce87cd7..ef01bcc9a9 100644 --- a/studio/backend/routes/auth.py +++ b/studio/backend/routes/auth.py @@ -9,6 +9,8 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response, status import ipaddress import os +import shlex +import sys import threading import time from collections import deque @@ -38,6 +40,36 @@ from auth.authentication import ( router = APIRouter() +def _reset_password_command() -> str: + """Shell command shown in the 'incorrect password' hint. + + Prefer the ABSOLUTE path to this install's ``unsloth`` launcher (a sibling + of the running interpreter) so the hint works even when the launcher's + directory is not on PATH -- e.g. a terminal opened before install, a stale + Windows PATH, or ``~/.local/bin`` not on PATH (the default on macOS) -- and + regardless of the current working directory. + + On POSIX the path is shell-quoted so spaces are handled. On Windows we only + use the bare absolute path when it has no spaces, because a quoted path needs + different syntax in cmd (``"..."``) vs PowerShell (``& "..."``); when it has + a space we fall back to the PATH-based form to stay unambiguous across + shells. If the launcher can't be located we fall back to the PATH form too. + """ + try: + bin_dir = os.path.dirname(os.path.abspath(sys.executable)) + if os.name == "nt": + exe = os.path.join(bin_dir, "unsloth.exe") + if os.path.isfile(exe) and " " not in exe: + return f"{exe} studio reset-password" + else: + exe = os.path.join(bin_dir, "unsloth") + if os.path.isfile(exe): + return f"{shlex.quote(exe)} studio reset-password" + except Exception: + pass + return "unsloth studio reset-password" + + # Per-(ip, username) bucket + per-IP aggregate. Account bucket stops one user's # typos from blocking others; the aggregate stops username-rotation spray. # Single-process only -- multi-worker deployments need a shared store. @@ -228,7 +260,7 @@ async def login(payload: AuthLoginRequest, request: Request) -> Token: _record_login_failure(unknown_key) raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, - detail = "Incorrect password. Run 'unsloth studio reset-password' in your terminal to reset it.", + detail = f"Incorrect password. Run '{_reset_password_command()}' in your terminal to reset it.", ) salt, pwd_hash, _jwt_secret, must_change_password = record @@ -236,7 +268,7 @@ async def login(payload: AuthLoginRequest, request: Request) -> Token: _record_login_failure(key) raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, - detail = "Incorrect password. Run 'unsloth studio reset-password' in your terminal to reset it.", + detail = f"Incorrect password. Run '{_reset_password_command()}' in your terminal to reset it.", ) _clear_login_bucket(key) From b516c12409959109474e56b40b99e2e9ba2300de Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 06:04:00 -0700 Subject: [PATCH 3/3] fix(studio): don't double-quote the reset-password hint for spaced paths Addresses review feedback on #5971. _reset_password_command() already shell-quotes the launcher path on POSIX (shlex.quote), so wrapping the result in another pair of single quotes in the error string produced a mangled hint for installs / home dirs containing spaces, e.g. Run ''/tmp/Unsloth Studio/.../unsloth' studio reset-password' in your terminal which a shell mis-parses. Drop the outer quotes and put the command at the end of the message so it is unambiguous and copy-pasteable in every case: Incorrect password. To reset it, run this in your terminal: Co-Authored-By: Claude Opus 4.8 --- studio/backend/routes/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/studio/backend/routes/auth.py b/studio/backend/routes/auth.py index ef01bcc9a9..23112ca2f8 100644 --- a/studio/backend/routes/auth.py +++ b/studio/backend/routes/auth.py @@ -260,7 +260,7 @@ async def login(payload: AuthLoginRequest, request: Request) -> Token: _record_login_failure(unknown_key) raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, - detail = f"Incorrect password. Run '{_reset_password_command()}' in your terminal to reset it.", + detail = f"Incorrect password. To reset it, run this in your terminal: {_reset_password_command()}", ) salt, pwd_hash, _jwt_secret, must_change_password = record @@ -268,7 +268,7 @@ async def login(payload: AuthLoginRequest, request: Request) -> Token: _record_login_failure(key) raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, - detail = f"Incorrect password. Run '{_reset_password_command()}' in your terminal to reset it.", + detail = f"Incorrect password. To reset it, run this in your terminal: {_reset_password_command()}", ) _clear_login_bucket(key)