Merge branch 'main' into feat/chat-ui-enhancements
This commit is contained in:
commit
2ea8217727
2 changed files with 437 additions and 46 deletions
46
.github/workflows/codeql.yml
vendored
46
.github/workflows/codeql.yml
vendored
|
|
@ -1,46 +0,0 @@
|
|||
---
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "25 14 * * 3"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
packages: read
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: python
|
||||
build-mode: none
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
...
|
||||
437
install.sh
437
install.sh
|
|
@ -89,6 +89,441 @@ _smart_apt_install() {
|
|||
fi
|
||||
}
|
||||
|
||||
# ── Helper: create desktop shortcuts and launcher script ──
|
||||
# Usage: create_studio_shortcuts <unsloth_exe> <os>
|
||||
# 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'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ai.unsloth.studio</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Unsloth Studio</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Unsloth Studio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>launch-studio</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
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"
|
||||
|
|
@ -251,6 +686,8 @@ echo "==> Running unsloth studio setup..."
|
|||
REQUESTED_PYTHON_VERSION="$(cd "$VENV_NAME/bin" && pwd)/python" \
|
||||
"$VENV_NAME/bin/unsloth" studio setup </dev/null
|
||||
|
||||
create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS"
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " Unsloth Studio installed!"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue