Merge pull request #80 from outis1one/claude/zealous-heisenberg-8ylloi

Claude/zealous heisenberg 8ylloi
This commit is contained in:
Outis
2026-06-19 22:56:34 -04:00
committed by GitHub
+267 -85
View File
@@ -507,84 +507,175 @@ UDEV
echo " GAME STORAGE LOCATION" echo " GAME STORAGE LOCATION"
echo "═══════════════════════════════════════════════════════" echo "═══════════════════════════════════════════════════════"
echo "" echo ""
echo " ROMs, Steam library, and saves all live under one directory."
log_info "Drives on this machine:" echo " Recommended: a large secondary drive so the OS SSD stays free."
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT | grep -v "^loop" | sed 's/^/ /'
echo ""
echo " ROMs, Steam library, and saves will be stored under one directory."
echo " Recommended: a larger/secondary drive (HDD) to keep the OS SSD free."
echo " The directory will be created if it doesn't exist."
echo " If it's on an unmounted drive the script will mount it and add it to fstab."
echo "" echo ""
local DEFAULT_STORAGE="$ACTUAL_HOME/drives/games" GAME_STORAGE_DIR="" # ── Build a numbered candidate list ──────────────────────────────────────
prompt_text " Game storage path [${DEFAULT_STORAGE}]:" "$DEFAULT_STORAGE" GAME_STORAGE_DIR # Slots: 0 = home dir, 1..N = mounted non-system partitions,
GAME_STORAGE_DIR="${GAME_STORAGE_DIR:-$DEFAULT_STORAGE}" # last = unmounted block devices (for formatting+mounting)
local -a _CAND_PATH _CAND_LABEL _CAND_DEV
local _ci=0
# Option 0 — home directory
local _home_free
_home_free=$(df -h "$ACTUAL_HOME" 2>/dev/null | awk 'NR==2{print $4}')
_CAND_PATH[0]="$ACTUAL_HOME/games"
_CAND_LABEL[0]="Home directory ($ACTUAL_HOME, ${_home_free:-?} free)"
_CAND_DEV[0]=""
_ci=1
# Mounted partitions — skip /, /boot*, /snap*, /tmp, home itself
local _skip_re="^(/|/boot|/snap|/tmp|/run|/sys|/proc|/dev)"
while IFS= read -r _mnt; do
[[ -z "$_mnt" ]] && continue
[[ "$_mnt" =~ $_skip_re ]] && continue
[[ "$_mnt" == "$ACTUAL_HOME" ]] && continue
local _dev _label _size _free
_dev=$(df "$_mnt" 2>/dev/null | awk 'NR==2{print $1}')
_label=$(lsblk -no LABEL "$_dev" 2>/dev/null | head -1)
_size=$(lsblk -no SIZE "$_dev" 2>/dev/null | head -1)
_free=$(df -h "$_mnt" 2>/dev/null | awk 'NR==2{print $4}')
local _display="${_label:-$(basename "$_dev")}"
_CAND_PATH[$_ci]="$_mnt/games"
_CAND_LABEL[$_ci]="$_mnt (${_display}, ${_size:-?} total, ${_free:-?} free)"
_CAND_DEV[$_ci]="$_dev"
((_ci++))
done < <(lsblk -no MOUNTPOINT 2>/dev/null | sort -u)
# Unmounted block devices (disks and partitions with no mountpoint)
local -a _UNMT_DEV _UNMT_LABEL
local _ui=0
while IFS= read -r _line; do
local _name _size _type _fstype _mnt _label
read -r _name _size _type _fstype _mnt _label <<< "$_line"
[[ "$_name" =~ ^loop ]] && continue
[[ "$_type" != "disk" && "$_type" != "part" ]] && continue
[[ -n "$_mnt" ]] && continue # already mounted → shown above
_UNMT_DEV[$_ui]="$_name"
_UNMT_LABEL[$_ui]="${_label:+$_label, }${_size} ${_fstype:+($_fstype)}"
((_ui++))
done < <(lsblk -no NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,LABEL 2>/dev/null)
# ── Display the menu ──────────────────────────────────────────────────────
echo " Mounted locations:"
local _n
for _n in "${!_CAND_PATH[@]}"; do
printf " %2d) %s\n" "$((_n + 1))" "${_CAND_LABEL[$_n]}"
printf " → %s\n" "${_CAND_PATH[$_n]}"
done
if [ "${#_UNMT_DEV[@]}" -gt 0 ]; then
echo ""
echo " Unmounted drives (script can format + mount):"
for _n in "${!_UNMT_DEV[@]}"; do
printf " U%d) /dev/%s %s\n" "$((_n + 1))" "${_UNMT_DEV[$_n]}" "${_UNMT_LABEL[$_n]}"
done
fi
echo ""
echo " c) Enter a custom path"
echo ""
local _PICK="" _BASE_PATH="" GAME_STORAGE_DIR=""
if [ "$UNATTENDED" = true ]; then
_BASE_PATH="${_CAND_PATH[0]}"
log_info "Unattended — using default: $_BASE_PATH"
else
while true; do
read -r -p " Select drive [1]: " _PICK
_PICK="${_PICK:-1}"
if [[ "$_PICK" =~ ^[0-9]+$ ]] && [ "$_PICK" -ge 1 ] && [ "$_PICK" -le "${#_CAND_PATH[@]}" ]; then
_BASE_PATH="${_CAND_PATH[$((_PICK - 1))]}"
break
elif [[ "${_PICK,,}" =~ ^u([0-9]+)$ ]]; then
local _uidx=$(( ${BASH_REMATCH[1]} - 1 ))
if [ "$_uidx" -ge 0 ] && [ "$_uidx" -lt "${#_UNMT_DEV[@]}" ]; then
_BASE_PATH="" # will be set after mounting below
break
fi
echo " Invalid selection — try again."
elif [[ "${_PICK,,}" == "c" ]]; then
_BASE_PATH=""
break
else
echo " Invalid selection — enter a number, U<n>, or c."
fi
done
fi
# ── Handle unmounted drive selection ─────────────────────────────────────
if [[ "${_PICK,,}" =~ ^u([0-9]+)$ ]]; then
local _uidx=$(( ${BASH_REMATCH[1]} - 1 ))
local _RAW_DEV="${_UNMT_DEV[$_uidx]}"
local _DEV="/dev/$_RAW_DEV"
local _DEFAULT_MP="$ACTUAL_HOME/drives/${_RAW_DEV%%[0-9]}"
local _MOUNT_POINT=""
prompt_text " Mount point for /dev/$_RAW_DEV [${_DEFAULT_MP}]:" "$_DEFAULT_MP" _MOUNT_POINT
_MOUNT_POINT="${_MOUNT_POINT:-$_DEFAULT_MP}"
local _PARTITION="$_DEV"
[[ "$_DEV" =~ [0-9]$ ]] || _PARTITION="${_DEV}1"
if ! blkid "$_PARTITION" &>/dev/null; then
log_info "Creating partition on $_DEV..."
printf 'g\nn\n1\n\n\nw\n' | fdisk "$_DEV"
partprobe "$_DEV"; sleep 2
fi
if ! blkid -s TYPE "$_PARTITION" 2>/dev/null | grep -q TYPE; then
log_info "Formatting $_PARTITION as ext4..."
mkfs.ext4 -F -L "games" "$_PARTITION"
else
log_info "$_PARTITION already has a filesystem — keeping existing data"
fi
mkdir -p "$_MOUNT_POINT"
mount "$_PARTITION" "$_MOUNT_POINT"
local _PART_UUID
_PART_UUID=$(blkid -s UUID -o value "$_PARTITION")
if [ -n "$_PART_UUID" ]; then
if grep -qs "$_PART_UUID" /etc/fstab; then
log_info "fstab: UUID=${_PART_UUID} already present"
else
echo "UUID=${_PART_UUID} ${_MOUNT_POINT} ext4 defaults,nofail 0 2" \
| tee -a /etc/fstab >/dev/null
log_success "fstab: UUID=${_PART_UUID}${_MOUNT_POINT}"
fi
else
log_warning "Could not read UUID — add /etc/fstab entry manually"
fi
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$_MOUNT_POINT" 2>/dev/null || true
log_success "$_PARTITION mounted at $_MOUNT_POINT"
_BASE_PATH="$_MOUNT_POINT/games"
fi
# ── Handle custom path ────────────────────────────────────────────────────
if [[ "${_PICK,,}" == "c" ]]; then
local _CUSTOM=""
prompt_text " Full game storage path:" "$ACTUAL_HOME/games" _CUSTOM
_BASE_PATH="${_CUSTOM:-$ACTUAL_HOME/games}"
_BASE_PATH="${_BASE_PATH/#\~/$ACTUAL_HOME}"
fi
# ── Let user confirm / edit the subdirectory ──────────────────────────────
# _BASE_PATH is now the full suggested path (e.g. /mnt/bigdrive/games).
# Show it and let the user change the trailing component.
echo ""
log_info "Suggested game storage path: $_BASE_PATH"
local _FINAL=""
prompt_text " Confirm or edit path [${_BASE_PATH}]:" "$_BASE_PATH" _FINAL
GAME_STORAGE_DIR="${_FINAL:-$_BASE_PATH}"
GAME_STORAGE_DIR="${GAME_STORAGE_DIR/#\~/$ACTUAL_HOME}" GAME_STORAGE_DIR="${GAME_STORAGE_DIR/#\~/$ACTUAL_HOME}"
# Check if the path crosses an unmounted drive log_success "Game storage: $GAME_STORAGE_DIR"
local _PARENT
_PARENT=$(dirname "$GAME_STORAGE_DIR")
if [ ! -d "$_PARENT" ]; then
log_warning "Parent directory $_PARENT does not exist."
echo ""
echo " If this path is on a separate drive, pick the device to mount:"
echo ""
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT | grep -v "^loop" | sed 's/^/ /'
echo ""
local RAW_DEV=""
prompt_text " Device to mount at ${GAME_STORAGE_DIR%/*} (e.g. sda, sdb1) or Enter to skip:" "" RAW_DEV
if [ -n "$RAW_DEV" ]; then
RAW_DEV="${RAW_DEV##/dev/}"
local DEV="/dev/$RAW_DEV"
local MOUNT_POINT="${GAME_STORAGE_DIR%/*}"
if [ -b "$DEV" ]; then
local PARTITION="${DEV}"
[[ "$DEV" =~ [0-9]$ ]] || PARTITION="${DEV}1"
if ! blkid "$PARTITION" &>/dev/null && \
! fdisk -l "$DEV" 2>/dev/null | grep -q "^${PARTITION}"; then
log_info "Creating partition on $DEV..."
printf 'g\nn\n1\n\n\nw\n' | fdisk "$DEV"
partprobe "$DEV"; sleep 2
fi
if ! blkid -s TYPE "$PARTITION" 2>/dev/null | grep -q TYPE; then
log_info "Formatting ${PARTITION} as ext4..."
mkfs.ext4 -F -L "games" "$PARTITION"
else
log_info "${PARTITION} already has a filesystem — keeping data"
fi
mkdir -p "$MOUNT_POINT"
mount "$PARTITION" "$MOUNT_POINT"
local PART_UUID
PART_UUID=$(blkid -s UUID -o value "$PARTITION")
if [ -n "$PART_UUID" ]; then
if grep -qs "$PART_UUID" /etc/fstab; then
log_info "fstab: UUID=${PART_UUID} already present"
else
echo "UUID=${PART_UUID} ${MOUNT_POINT} ext4 defaults,nofail 0 2" \
| tee -a /etc/fstab >/dev/null
log_success "fstab: UUID=${PART_UUID}${MOUNT_POINT} (nofail, auto-mount on boot)"
fi
else
log_warning "Could not read UUID for ${PARTITION} — add /etc/fstab entry manually"
fi
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MOUNT_POINT" 2>/dev/null || true
log_success "${PARTITION} mounted at ${MOUNT_POINT}"
else
log_warning "$DEV not found — continuing, ensure drive is mounted before starting Wolf"
fi
fi
fi
# Create the storage sub-directories # Create the storage sub-directories
mkdir -p "$GAME_STORAGE_DIR/roms" "$GAME_STORAGE_DIR/steam" \ mkdir -p "$GAME_STORAGE_DIR/roms" "$GAME_STORAGE_DIR/steam" \
"$GAME_STORAGE_DIR/saves" "$GAME_STORAGE_DIR/media" "$GAME_STORAGE_DIR/saves" "$GAME_STORAGE_DIR/media"
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$GAME_STORAGE_DIR" 2>/dev/null || true
log_success "Storage layout: $GAME_STORAGE_DIR/{roms,steam,saves,media}" log_success "Storage layout: $GAME_STORAGE_DIR/{roms,steam,saves,media}"
# ── docker-compose.yml ──────────────────────────────────────────────────── # ── docker-compose.yml ────────────────────────────────────────────────────
@@ -658,6 +749,15 @@ volumes:
EOF EOF
log_success "docker-compose.yml created" log_success "docker-compose.yml created"
# Save the game storage path so it's visible and editable later
cat > .env << EOF
# Wolf game/ROM storage root — edit this and run ./manage.sh update-storage to apply
GAME_STORAGE_DIR=${GAME_STORAGE_DIR}
EOF
chmod 600 .env
chown "$ACTUAL_USER:$ACTUAL_USER" .env
log_success ".env written with GAME_STORAGE_DIR=${GAME_STORAGE_DIR}"
# ── Firewall ────────────────────────────────────────────────────────────── # ── Firewall ──────────────────────────────────────────────────────────────
if command -v ufw &>/dev/null; then if command -v ufw &>/dev/null; then
log_info "Opening Moonlight ports in UFW..." log_info "Opening Moonlight ports in UFW..."
@@ -682,18 +782,40 @@ case "$1" in
docker compose pull docker compose pull
docker compose up -d docker compose up -d
;; ;;
add-apps) add-apps|update-storage)
WOLF_CFG=/etc/wolf/cfg/config.toml WOLF_CFG=/etc/wolf/cfg/config.toml
GAME_DIR="${2}" GAME_DIR="${2}"
# update-storage: read path from .env if not given on command line
if [ "$1" = "update-storage" ] && [ -z "$GAME_DIR" ]; then
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
if [ -f "$ENV_FILE" ]; then
GAME_DIR=$(grep '^GAME_STORAGE_DIR=' "$ENV_FILE" | cut -d= -f2-)
fi
fi
if [ -z "$GAME_DIR" ]; then if [ -z "$GAME_DIR" ]; then
echo "Usage: ./manage.sh add-apps /path/to/game/storage" echo "Usage: ./manage.sh add-apps <path>"
echo " e.g. ./manage.sh add-apps /home/user/drives/games" echo " ./manage.sh update-storage [<new-path>]"
echo ""
echo " <path> root of game storage, e.g. /home/user/drives/games"
echo " update-storage with no path reads GAME_STORAGE_DIR from .env"
exit 1 exit 1
fi fi
if [ ! -f "$WOLF_CFG" ]; then if [ ! -f "$WOLF_CFG" ]; then
echo "Wolf config not found at $WOLF_CFG — is Wolf running?" echo "Wolf config not found at $WOLF_CFG — is Wolf running?"
exit 1 exit 1
fi fi
# Persist new path in .env when update-storage is called with explicit path
if [ "$1" = "update-storage" ] && [ -n "${2:-}" ]; then
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
sed -i "s|^GAME_STORAGE_DIR=.*|GAME_STORAGE_DIR=${GAME_DIR}|" "$SCRIPT_DIR/.env" 2>/dev/null \
|| echo "GAME_STORAGE_DIR=${GAME_DIR}" >> "$SCRIPT_DIR/.env"
echo "Updated .env: GAME_STORAGE_DIR=${GAME_DIR}"
fi
python3 - "$GAME_DIR" "$WOLF_CFG" << 'PYEOF' python3 - "$GAME_DIR" "$WOLF_CFG" << 'PYEOF'
import sys import sys
@@ -713,6 +835,30 @@ for i, line in enumerate(lines):
if first_start is None: if first_start is None:
print('ERROR: no [[profiles]] section found'); sys.exit(1) print('ERROR: no [[profiles]] section found'); sys.exit(1)
def update_mounts(lines, app_name, new_mounts_line):
for i, line in enumerate(lines):
if f"name = '{app_name}'" in line:
# scan forward for the mounts line within this app block
for j in range(i - 10, i + 10):
if 0 <= j < len(lines) and lines[j].strip().startswith('mounts ='):
lines[j] = new_mounts_line
return True
return False
first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
updated = []
if has_steam:
new_line = f" mounts = [ '{games}/steam:/home/retro/.steam:rw' ]\n"
if update_mounts(lines, 'WolfSteam', new_line):
updated.append('Steam mounts updated')
if has_esde:
new_line = f" mounts = [ '{games}/roms:/ROMs:rw', '{games}/saves:/home/retro/.config/retroarch/saves:rw', '{games}/media:/media:rw' ]\n"
if update_mounts(lines, 'WolfES-DE', new_line):
updated.append('ES-DE mounts updated')
first_block = lines[first_start:insert_at] first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block) has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block) has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
@@ -779,8 +925,12 @@ if to_insert:
f.writelines(new_lines) f.writelines(new_lines)
added = [n for n, exists in [('Steam', has_steam), ('EmulationStation', has_esde)] if not exists] added = [n for n, exists in [('Steam', has_steam), ('EmulationStation', has_esde)] if not exists]
print(f"Added to default profile: {', '.join(added)}") print(f"Added to default profile: {', '.join(added)}")
elif updated:
with open(cfg, 'w') as f:
f.writelines(lines)
print('; '.join(updated))
else: else:
print('Both apps already in default profile') print('Both apps already in profile (paths unchanged)')
PYEOF PYEOF
docker compose restart wolf docker compose restart wolf
echo "Wolf restarted. Steam and EmulationStation should now appear in Moonlight." echo "Wolf restarted. Steam and EmulationStation should now appear in Moonlight."
@@ -819,15 +969,17 @@ PYEOF
;; ;;
*) *)
echo "Wolf Cloud Gaming" echo "Wolf Cloud Gaming"
echo " ./manage.sh start - Start Wolf" echo " ./manage.sh start - Start Wolf"
echo " ./manage.sh stop - Stop Wolf" echo " ./manage.sh stop - Stop Wolf"
echo " ./manage.sh restart - Restart Wolf" echo " ./manage.sh restart - Restart Wolf"
echo " ./manage.sh logs - Follow Wolf logs" echo " ./manage.sh logs - Follow Wolf logs"
echo " ./manage.sh status - Show Wolf + app containers" echo " ./manage.sh status - Show Wolf + app containers"
echo " ./manage.sh pin - Show recent Moonlight pairing PIN link" echo " ./manage.sh pin - Show recent Moonlight pairing PIN link"
echo " ./manage.sh update - Pull latest Wolf image and restart" echo " ./manage.sh update - Pull latest Wolf image and restart"
echo " ./manage.sh add-apps <path> - Add Steam + ES-DE to Wolf config" echo " ./manage.sh add-apps <path> - Add Steam + ES-DE to Wolf config"
echo " ./manage.sh backup - How to set up backups (sudo ./setup.sh backup)" echo " ./manage.sh update-storage [<new-path>] - Update game storage path in Wolf config"
echo " (reads .env if no path given)"
echo " ./manage.sh backup - How to set up backups"
;; ;;
esac esac
MEOF MEOF
@@ -850,7 +1002,7 @@ MEOF
if [ -f "$WOLF_CFG" ]; then if [ -f "$WOLF_CFG" ]; then
log_info "Adding Steam and EmulationStation to Wolf config..." log_info "Adding Steam and EmulationStation to Wolf config..."
python3 - "$GAME_STORAGE_DIR" "$WOLF_CFG" << 'PYEOF' python3 - "$GAME_STORAGE_DIR" "$WOLF_CFG" << 'PYEOF'
import sys import sys, re
games = sys.argv[1].rstrip('/') games = sys.argv[1].rstrip('/')
cfg = sys.argv[2] cfg = sys.argv[2]
@@ -875,6 +1027,32 @@ first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block) has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block) has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
# If an app already exists, update its mounts line in place rather than skip.
def update_mounts(lines, app_name, new_mounts_line):
in_app = False
for i, line in enumerate(lines):
if f"name = '{app_name}'" in line:
in_app = True
if in_app and line.strip().startswith('mounts ='):
lines[i] = new_mounts_line
return True
return False
updated = []
if has_steam:
new_line = f" mounts = [ '{games}/steam:/home/retro/.steam:rw' ]\n"
if update_mounts(lines, 'WolfSteam', new_line):
updated.append('Steam mounts updated')
if has_esde:
new_line = f" mounts = [ '{games}/roms:/ROMs:rw', '{games}/saves:/home/retro/.config/retroarch/saves:rw', '{games}/media:/media:rw' ]\n"
if update_mounts(lines, 'WolfES-DE', new_line):
updated.append('ES-DE mounts updated')
# Re-derive first_block after in-place updates
first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
to_insert = [] to_insert = []
if not has_steam: if not has_steam:
to_insert += [ to_insert += [
@@ -937,8 +1115,12 @@ if to_insert:
f.writelines(new_lines) f.writelines(new_lines)
added = [n for n, exists in [('Steam', has_steam), ('EmulationStation', has_esde)] if not exists] added = [n for n, exists in [('Steam', has_steam), ('EmulationStation', has_esde)] if not exists]
print(f"[INFO] Added to default profile: {', '.join(added)}") print(f"[INFO] Added to default profile: {', '.join(added)}")
elif updated:
with open(cfg, 'w') as f:
f.writelines(lines)
print(f"[INFO] {'; '.join(updated)}")
else: else:
print('[INFO] Steam and EmulationStation already in default profile') print('[INFO] Steam and EmulationStation already in default profile (paths unchanged)')
PYEOF PYEOF
docker compose restart wolf docker compose restart wolf
log_success "Wolf restarted with updated config" log_success "Wolf restarted with updated config"